@aljazmc/testero 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -91
- package/bin/main.js +1427 -472
- 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,18 +89,28 @@ 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 {
|
|
106
|
+
import {
|
|
107
|
+
existsSync,
|
|
108
|
+
readdirSync,
|
|
109
|
+
readFileSync,
|
|
110
|
+
realpathSync,
|
|
111
|
+
statSync,
|
|
112
|
+
writeFileSync
|
|
113
|
+
} from "node:fs";
|
|
89
114
|
import { join } from "node:path";
|
|
90
115
|
import { pathToFileURL } from "node:url";
|
|
91
116
|
var Config;
|
|
@@ -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,93 @@ ${items.map((item) => `${inner}${item},
|
|
|
188
233
|
merged[key] = _Config.merge(base[key], value);
|
|
189
234
|
return merged;
|
|
190
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* The files that are not ignored, relative to the root. Symlinks are
|
|
238
|
+
* followed, a folder by every path to it, but not into a folder it is in,
|
|
239
|
+
* which would never end; broken symlinks and unreadable folders are
|
|
240
|
+
* reported and left out, like pipes and other special files.
|
|
241
|
+
*/
|
|
191
242
|
*files(directory = "") {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
243
|
+
yield* this.#walk(directory, /* @__PURE__ */ new Set());
|
|
244
|
+
}
|
|
245
|
+
*#walk(directory, ancestors) {
|
|
246
|
+
const absolute = join(this.root, directory);
|
|
247
|
+
let entries;
|
|
248
|
+
let real;
|
|
249
|
+
try {
|
|
250
|
+
real = realpathSync(absolute);
|
|
251
|
+
if (ancestors.has(real)) return;
|
|
252
|
+
entries = readdirSync(absolute, { withFileTypes: true });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
this.warn(
|
|
255
|
+
`${directory || "."}: the folder cannot be read (${_Config.#reason(error)}), left out.`
|
|
256
|
+
);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
ancestors.add(real);
|
|
195
260
|
for (const entry of entries) {
|
|
196
261
|
const path = directory ? `${directory}/${entry.name}` : entry.name;
|
|
197
|
-
|
|
198
|
-
if (entry.
|
|
199
|
-
|
|
262
|
+
let kind = entry.isDirectory() ? "directory" : entry.isFile() ? "file" : "other";
|
|
263
|
+
if (entry.isSymbolicLink())
|
|
264
|
+
try {
|
|
265
|
+
const target = statSync(join(this.root, path));
|
|
266
|
+
kind = target.isDirectory() ? "directory" : target.isFile() ? "file" : "other";
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (!this.ignore.matches(path))
|
|
269
|
+
this.warn(
|
|
270
|
+
`${path}: a broken symlink (${_Config.#reason(error)}), left out.`
|
|
271
|
+
);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (this.ignore.matches(path, kind === "directory")) continue;
|
|
275
|
+
if (kind === "directory") yield* this.#walk(path, ancestors);
|
|
276
|
+
else if (kind === "file") yield path;
|
|
200
277
|
}
|
|
278
|
+
ancestors.delete(real);
|
|
201
279
|
}
|
|
202
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Text files that are not ignored and that accepts takes. Binaries, with
|
|
282
|
+
* a NUL byte, are skipped; files that are not UTF-8 are reported and
|
|
283
|
+
* skipped, as writing them back as UTF-8 would change their text.
|
|
284
|
+
*/
|
|
203
285
|
*sources(accepts = () => true) {
|
|
286
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
204
287
|
for (const file of this.files()) {
|
|
205
288
|
if (!accepts(file)) continue;
|
|
206
289
|
const path = join(this.root, file);
|
|
207
|
-
|
|
208
|
-
|
|
290
|
+
let bytes;
|
|
291
|
+
try {
|
|
292
|
+
bytes = readFileSync(path);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
this.warn(
|
|
295
|
+
`${file}: the file cannot be read (${_Config.#reason(error)}), left out.`
|
|
296
|
+
);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (bytes.includes(0)) continue;
|
|
300
|
+
let text;
|
|
301
|
+
try {
|
|
302
|
+
text = utf8.decode(bytes);
|
|
303
|
+
} catch {
|
|
304
|
+
this.warn(`${file}: not UTF-8, left out.`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
yield { file, path, text };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Why a file system call failed, in words. */
|
|
311
|
+
static #reason(error) {
|
|
312
|
+
const code = error?.code;
|
|
313
|
+
switch (code) {
|
|
314
|
+
case "ENOENT":
|
|
315
|
+
return "its target does not exist";
|
|
316
|
+
case "EACCES":
|
|
317
|
+
case "EPERM":
|
|
318
|
+
return "no permission";
|
|
319
|
+
case "ELOOP":
|
|
320
|
+
return "a loop of symlinks";
|
|
321
|
+
default:
|
|
322
|
+
return error instanceof Error ? error.message : String(error);
|
|
209
323
|
}
|
|
210
324
|
}
|
|
211
325
|
};
|
|
@@ -223,9 +337,10 @@ var init_includes = __esm({
|
|
|
223
337
|
__name(this, "Includes");
|
|
224
338
|
}
|
|
225
339
|
#globs;
|
|
226
|
-
|
|
340
|
+
/** With the name of the setting the globs come from, for their errors. */
|
|
341
|
+
constructor(globs, setting = "includes") {
|
|
227
342
|
this.#globs = globs.map((glob) => ({
|
|
228
|
-
pattern: new Pattern(glob.replace(/^!/, "")),
|
|
343
|
+
pattern: new Pattern(glob.replace(/^!/, ""), setting),
|
|
229
344
|
included: !glob.startsWith("!")
|
|
230
345
|
}));
|
|
231
346
|
}
|
|
@@ -249,11 +364,152 @@ var init_src = __esm({
|
|
|
249
364
|
}
|
|
250
365
|
});
|
|
251
366
|
|
|
367
|
+
// packages/testero/src/expect/equality.ts
|
|
368
|
+
var Equality;
|
|
369
|
+
var init_equality = __esm({
|
|
370
|
+
"packages/testero/src/expect/equality.ts"() {
|
|
371
|
+
"use strict";
|
|
372
|
+
Equality = class _Equality {
|
|
373
|
+
static {
|
|
374
|
+
__name(this, "Equality");
|
|
375
|
+
}
|
|
376
|
+
/** Whether a value matches by its own rule, like `expect.any(Number)`. */
|
|
377
|
+
static asymmetric(value) {
|
|
378
|
+
return typeof value === "object" && value !== null && typeof value.asymmetricMatch === "function";
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Whether two values are deeply equal. Loosely, properties set to
|
|
382
|
+
* undefined count as missing; strictly, types and holes in arrays count.
|
|
383
|
+
*/
|
|
384
|
+
static equals(a, b, strict = false) {
|
|
385
|
+
return _Equality.#equals(a, b, strict, [], []);
|
|
386
|
+
}
|
|
387
|
+
/** Whether received has every property expected has, recursively, like `toMatchObject`. */
|
|
388
|
+
static matches(received, expected) {
|
|
389
|
+
if (_Equality.asymmetric(expected))
|
|
390
|
+
return expected.asymmetricMatch(received);
|
|
391
|
+
if (!_Equality.#plainish(expected) || typeof received !== "object" || received === null)
|
|
392
|
+
return _Equality.equals(received, expected);
|
|
393
|
+
if (Array.isArray(expected))
|
|
394
|
+
return Array.isArray(received) && received.length === expected.length && expected.every(
|
|
395
|
+
(item, index) => _Equality.matches(received[index], item)
|
|
396
|
+
);
|
|
397
|
+
return _Equality.#keys(expected, true).every(
|
|
398
|
+
(key) => key in received && _Equality.matches(
|
|
399
|
+
received[key],
|
|
400
|
+
expected[key]
|
|
401
|
+
)
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
/** Objects and arrays that `toMatchObject` compares by their properties. */
|
|
405
|
+
static #plainish(value) {
|
|
406
|
+
return typeof value === "object" && value !== null && !(value instanceof Date) && !(value instanceof RegExp) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Error);
|
|
407
|
+
}
|
|
408
|
+
static #equals(a, b, strict, aSeen, bSeen) {
|
|
409
|
+
if (_Equality.asymmetric(a)) return a.asymmetricMatch(b);
|
|
410
|
+
if (_Equality.asymmetric(b)) return b.asymmetricMatch(a);
|
|
411
|
+
if (Object.is(a, b)) return true;
|
|
412
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
413
|
+
return false;
|
|
414
|
+
const tag = Object.prototype.toString.call(a);
|
|
415
|
+
if (tag !== Object.prototype.toString.call(b)) return false;
|
|
416
|
+
if (strict && Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
|
|
417
|
+
return false;
|
|
418
|
+
const bytes = _Equality.bytes(a);
|
|
419
|
+
if (bytes) return _Equality.#sameBytes(bytes, _Equality.bytes(b));
|
|
420
|
+
if (a instanceof URL) return a.href === b.href;
|
|
421
|
+
if (a instanceof Date)
|
|
422
|
+
return Object.is(a.getTime(), b.getTime());
|
|
423
|
+
if (a instanceof RegExp) return String(a) === String(b);
|
|
424
|
+
if (a instanceof Error && b instanceof Error)
|
|
425
|
+
return a.name === b.name && a.message === b.message;
|
|
426
|
+
if (a instanceof Number || a instanceof String || a instanceof Boolean)
|
|
427
|
+
return Object.is(
|
|
428
|
+
a.valueOf(),
|
|
429
|
+
b.valueOf()
|
|
430
|
+
);
|
|
431
|
+
const index = aSeen.indexOf(a);
|
|
432
|
+
if (index >= 0) return bSeen[index] === b;
|
|
433
|
+
aSeen.push(a);
|
|
434
|
+
bSeen.push(b);
|
|
435
|
+
try {
|
|
436
|
+
return _Equality.#structures(a, b, strict, aSeen, bSeen);
|
|
437
|
+
} finally {
|
|
438
|
+
aSeen.pop();
|
|
439
|
+
bSeen.pop();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/** The bytes of an ArrayBuffer, SharedArrayBuffer or DataView, undefined for other values. */
|
|
443
|
+
static bytes(value) {
|
|
444
|
+
if (value instanceof DataView)
|
|
445
|
+
return new Uint8Array(
|
|
446
|
+
value.buffer,
|
|
447
|
+
value.byteOffset,
|
|
448
|
+
value.byteLength
|
|
449
|
+
);
|
|
450
|
+
if (value instanceof ArrayBuffer || typeof SharedArrayBuffer === "function" && value instanceof SharedArrayBuffer)
|
|
451
|
+
return new Uint8Array(value);
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
static #sameBytes(a, b) {
|
|
455
|
+
return b !== void 0 && a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
456
|
+
}
|
|
457
|
+
static #structures(a, b, strict, aSeen, bSeen) {
|
|
458
|
+
const equal = /* @__PURE__ */ __name((x, y) => _Equality.#equals(x, y, strict, aSeen, bSeen), "equal");
|
|
459
|
+
if (a instanceof Map && b instanceof Map)
|
|
460
|
+
return a.size === b.size && [...a].every(
|
|
461
|
+
([key, value]) => b.has(key) ? equal(value, b.get(key)) : [...b].some(
|
|
462
|
+
([other, item]) => equal(key, other) && equal(value, item)
|
|
463
|
+
)
|
|
464
|
+
);
|
|
465
|
+
if (a instanceof Set && b instanceof Set)
|
|
466
|
+
return a.size === b.size && [...a].every(
|
|
467
|
+
(value) => b.has(value) || [...b].some((other) => equal(value, other))
|
|
468
|
+
);
|
|
469
|
+
if (Array.isArray(a) || ArrayBuffer.isView(a)) {
|
|
470
|
+
const left = a;
|
|
471
|
+
const right = b;
|
|
472
|
+
if (left.length !== right.length) return false;
|
|
473
|
+
for (let index = 0; index < left.length; index++) {
|
|
474
|
+
if (strict && index in left !== index in right) return false;
|
|
475
|
+
if (!equal(left[index], right[index])) return false;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const indexed = Array.isArray(a) || ArrayBuffer.isView(a);
|
|
479
|
+
const keysOf = /* @__PURE__ */ __name((object) => _Equality.#keys(object, strict).filter(
|
|
480
|
+
(key) => !(indexed && typeof key === "string" && /^\d+$/.test(key))
|
|
481
|
+
), "keysOf");
|
|
482
|
+
const aKeys = keysOf(a);
|
|
483
|
+
const bKeys = keysOf(b);
|
|
484
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
485
|
+
return aKeys.every(
|
|
486
|
+
(key) => Object.hasOwn(b, key) && equal(
|
|
487
|
+
a[key],
|
|
488
|
+
b[key]
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
/** Own enumerable keys, without the ones set to undefined unless strict. */
|
|
493
|
+
static #keys(object, strict) {
|
|
494
|
+
const record = object;
|
|
495
|
+
const keys = [
|
|
496
|
+
...Object.keys(object),
|
|
497
|
+
...Object.getOwnPropertySymbols(object).filter(
|
|
498
|
+
(symbol) => Object.getOwnPropertyDescriptor(object, symbol)?.enumerable
|
|
499
|
+
)
|
|
500
|
+
];
|
|
501
|
+
return strict ? keys : keys.filter((key) => record[key] !== void 0);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
252
507
|
// packages/testero/src/expect/format.ts
|
|
253
508
|
var Format;
|
|
254
509
|
var init_format = __esm({
|
|
255
510
|
"packages/testero/src/expect/format.ts"() {
|
|
256
511
|
"use strict";
|
|
512
|
+
init_equality();
|
|
257
513
|
Format = class _Format {
|
|
258
514
|
static {
|
|
259
515
|
__name(this, "Format");
|
|
@@ -325,6 +581,7 @@ Received: ${_Format.inline(received)}`;
|
|
|
325
581
|
if (value instanceof Date)
|
|
326
582
|
return Number.isNaN(value.getTime()) ? "Date { NaN }" : value.toISOString();
|
|
327
583
|
if (value instanceof RegExp) return String(value);
|
|
584
|
+
if (value instanceof URL) return `URL ${JSON.stringify(value.href)}`;
|
|
328
585
|
if (value instanceof Error) return `[${value.name}: ${value.message}]`;
|
|
329
586
|
if (seen.includes(value)) return "[Circular]";
|
|
330
587
|
const inner = [...seen, value];
|
|
@@ -348,6 +605,15 @@ Received: ${_Format.inline(received)}`;
|
|
|
348
605
|
indent,
|
|
349
606
|
multiline
|
|
350
607
|
);
|
|
608
|
+
const bytes = Equality.bytes(value);
|
|
609
|
+
if (bytes)
|
|
610
|
+
return _Format.#block(
|
|
611
|
+
`${name ?? "ArrayBuffer"} [`,
|
|
612
|
+
"]",
|
|
613
|
+
Array.from(bytes, String),
|
|
614
|
+
indent,
|
|
615
|
+
multiline
|
|
616
|
+
);
|
|
351
617
|
if (Array.isArray(value) || ArrayBuffer.isView(value)) {
|
|
352
618
|
const items = Array.from(value, print);
|
|
353
619
|
const prefix2 = name && name !== "Array" ? `${name} [` : "[";
|
|
@@ -392,7 +658,11 @@ var init_each = __esm({
|
|
|
392
658
|
*/
|
|
393
659
|
static rows(table, values) {
|
|
394
660
|
if (_Each.#template(table)) {
|
|
395
|
-
const names = table
|
|
661
|
+
const names = (table[0] ?? "").trim().split(/\s*\|\s*/);
|
|
662
|
+
if (values.length % names.length !== 0)
|
|
663
|
+
throw new TypeError(
|
|
664
|
+
`each table has ${names.length} columns (${names.join(", ")}), but ${values.length} values do not fill whole rows`
|
|
665
|
+
);
|
|
396
666
|
const rows = [];
|
|
397
667
|
for (let index = 0; index < values.length; index += names.length)
|
|
398
668
|
rows.push([
|
|
@@ -588,7 +858,8 @@ var init_collector = __esm({
|
|
|
588
858
|
for (const [index, row] of Each.rows(table, values).entries())
|
|
589
859
|
this.#test(
|
|
590
860
|
Each.title(String(name), row, index),
|
|
591
|
-
|
|
861
|
+
// a parameter after the row's values is the done callback
|
|
862
|
+
body.length > row.length ? (done) => body(...row, done) : () => body(...row),
|
|
592
863
|
mode,
|
|
593
864
|
timeout
|
|
594
865
|
);
|
|
@@ -714,9 +985,14 @@ var init_mock = __esm({
|
|
|
714
985
|
this.state.instances.push(context);
|
|
715
986
|
this.state.results.push(result);
|
|
716
987
|
this.state.lastCall = args;
|
|
988
|
+
const index = this.state.instances.length - 1;
|
|
717
989
|
const implementation = this.#once.shift() ?? this.#implementation;
|
|
718
990
|
try {
|
|
719
991
|
result.value = !implementation ? void 0 : target ? Reflect.construct(implementation, args, target) : implementation.apply(context, args);
|
|
992
|
+
if (target && typeof result.value === "object" && result.value) {
|
|
993
|
+
this.state.instances[index] = result.value;
|
|
994
|
+
this.state.contexts[index] = result.value;
|
|
995
|
+
}
|
|
720
996
|
result.type = "return";
|
|
721
997
|
return result.value;
|
|
722
998
|
} catch (error) {
|
|
@@ -788,123 +1064,617 @@ var init_mock = __esm({
|
|
|
788
1064
|
}
|
|
789
1065
|
});
|
|
790
1066
|
|
|
791
|
-
// packages/testero/src/
|
|
792
|
-
var
|
|
793
|
-
var
|
|
794
|
-
"packages/testero/src/
|
|
1067
|
+
// packages/testero/src/globals.ts
|
|
1068
|
+
var Globals;
|
|
1069
|
+
var init_globals = __esm({
|
|
1070
|
+
"packages/testero/src/globals.ts"() {
|
|
795
1071
|
"use strict";
|
|
796
|
-
|
|
1072
|
+
init_mock();
|
|
1073
|
+
Globals = class _Globals {
|
|
797
1074
|
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, [], []);
|
|
1075
|
+
__name(this, "Globals");
|
|
810
1076
|
}
|
|
811
|
-
|
|
812
|
-
static
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1077
|
+
static key = /* @__PURE__ */ Symbol.for("testero");
|
|
1078
|
+
static names = [
|
|
1079
|
+
"describe",
|
|
1080
|
+
"fdescribe",
|
|
1081
|
+
"xdescribe",
|
|
1082
|
+
"test",
|
|
1083
|
+
"it",
|
|
1084
|
+
"fit",
|
|
1085
|
+
"xit",
|
|
1086
|
+
"xtest",
|
|
1087
|
+
"beforeAll",
|
|
1088
|
+
"afterAll",
|
|
1089
|
+
"beforeEach",
|
|
1090
|
+
"afterEach",
|
|
1091
|
+
"expect",
|
|
1092
|
+
"testero",
|
|
1093
|
+
"jest"
|
|
1094
|
+
];
|
|
1095
|
+
/** The module the transformer injects, it reads the globals of the file being run. */
|
|
1096
|
+
static source() {
|
|
1097
|
+
return [
|
|
1098
|
+
'const globals = globalThis[Symbol.for("testero")];',
|
|
1099
|
+
..._Globals.names.map(
|
|
1100
|
+
(name) => `export const ${name} = globals.${name};`
|
|
825
1101
|
)
|
|
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);
|
|
1102
|
+
].join("\n");
|
|
831
1103
|
}
|
|
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
|
-
|
|
1104
|
+
/** Sets the globals for the next test file to import. */
|
|
1105
|
+
static install(collector, expect, settings) {
|
|
1106
|
+
const { describe, test } = collector;
|
|
1107
|
+
const testero = {
|
|
1108
|
+
fn: Mocks.fn,
|
|
1109
|
+
spyOn: Mocks.spyOn,
|
|
1110
|
+
isMockFunction: Mocks.isMockFunction,
|
|
1111
|
+
clearAllMocks: /* @__PURE__ */ __name(() => {
|
|
1112
|
+
Mocks.clearAll();
|
|
1113
|
+
return testero;
|
|
1114
|
+
}, "clearAllMocks"),
|
|
1115
|
+
resetAllMocks: /* @__PURE__ */ __name(() => {
|
|
1116
|
+
Mocks.resetAll();
|
|
1117
|
+
return testero;
|
|
1118
|
+
}, "resetAllMocks"),
|
|
1119
|
+
restoreAllMocks: /* @__PURE__ */ __name(() => {
|
|
1120
|
+
Mocks.restoreAll();
|
|
1121
|
+
return testero;
|
|
1122
|
+
}, "restoreAllMocks"),
|
|
1123
|
+
/** Sets the timeout of the tests and hooks of this file. */
|
|
1124
|
+
setTimeout: /* @__PURE__ */ __name((timeout) => {
|
|
1125
|
+
settings.timeout = timeout;
|
|
1126
|
+
return testero;
|
|
1127
|
+
}, "setTimeout")
|
|
1128
|
+
};
|
|
1129
|
+
globalThis[_Globals.key] = {
|
|
1130
|
+
describe,
|
|
1131
|
+
fdescribe: describe.only,
|
|
1132
|
+
xdescribe: describe.skip,
|
|
1133
|
+
test,
|
|
1134
|
+
it: test,
|
|
1135
|
+
fit: test.only,
|
|
1136
|
+
xit: test.skip,
|
|
1137
|
+
xtest: test.skip,
|
|
1138
|
+
beforeAll: collector.hook("beforeAll"),
|
|
1139
|
+
afterAll: collector.hook("afterAll"),
|
|
1140
|
+
beforeEach: collector.hook("beforeEach"),
|
|
1141
|
+
afterEach: collector.hook("afterEach"),
|
|
1142
|
+
expect: expect.create(),
|
|
1143
|
+
testero,
|
|
1144
|
+
jest: testero
|
|
1145
|
+
};
|
|
862
1146
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
// packages/testero/src/transformer.ts
|
|
1152
|
+
import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1153
|
+
import { tmpdir } from "node:os";
|
|
1154
|
+
import { dirname, extname, join as join2, resolve, sep } from "node:path";
|
|
1155
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1156
|
+
import { build } from "esbuild";
|
|
1157
|
+
var Transformer;
|
|
1158
|
+
var init_transformer = __esm({
|
|
1159
|
+
"packages/testero/src/transformer.ts"() {
|
|
1160
|
+
"use strict";
|
|
1161
|
+
init_globals();
|
|
1162
|
+
Transformer = class _Transformer {
|
|
1163
|
+
static {
|
|
1164
|
+
__name(this, "Transformer");
|
|
1165
|
+
}
|
|
1166
|
+
/** Packages test files import the globals from, like with Jest. */
|
|
1167
|
+
static modules = ["@jest/globals", "@aljazmc/testero"];
|
|
1168
|
+
static #loaders = {
|
|
1169
|
+
".js": "js",
|
|
1170
|
+
".cjs": "js",
|
|
1171
|
+
".mjs": "js",
|
|
1172
|
+
".jsx": "jsx",
|
|
1173
|
+
".ts": "ts",
|
|
1174
|
+
".cts": "ts",
|
|
1175
|
+
".mts": "ts",
|
|
1176
|
+
".tsx": "tsx"
|
|
1177
|
+
};
|
|
1178
|
+
/** The import.meta properties that describe the source file. */
|
|
1179
|
+
static #meta = ["url", "filename", "dirname"];
|
|
1180
|
+
/** CommonJS globals the ES module bundle lacks, mapped to the same descriptions. */
|
|
1181
|
+
static #commonjs = {
|
|
1182
|
+
__dirname: "dirname",
|
|
1183
|
+
__filename: "filename"
|
|
1184
|
+
};
|
|
1185
|
+
/** Sources that refer to the location of their own file. */
|
|
1186
|
+
static #located = /\bimport\.meta\b|\b__dirname\b|\b__filename\b/;
|
|
1187
|
+
/** Marks the resolutions the plugin asks esbuild for itself. */
|
|
1188
|
+
static #own = /* @__PURE__ */ Symbol("testero");
|
|
1189
|
+
/** Where compiled test files go, stack frames there are testero's own. */
|
|
1190
|
+
static prefix = join2(tmpdir(), "testero-build-");
|
|
1191
|
+
#directory = mkdtempSync(_Transformer.prefix);
|
|
1192
|
+
#globals = join2(this.#directory, "globals.mjs");
|
|
1193
|
+
#count = 0;
|
|
1194
|
+
/** The code of each compiled module by its path, with an inline source map, for coverage. */
|
|
1195
|
+
bundles = /* @__PURE__ */ new Map();
|
|
1196
|
+
#keep;
|
|
1197
|
+
/** Keeps the code of the compiled modules in `bundles` when told. */
|
|
1198
|
+
constructor(keep = false) {
|
|
1199
|
+
this.#keep = keep;
|
|
1200
|
+
writeFileSync2(this.#globals, Globals.source());
|
|
1201
|
+
}
|
|
1202
|
+
/** Bundles a test file, returns the URL of the module to import. */
|
|
1203
|
+
async compile(file) {
|
|
1204
|
+
const outfile = join2(this.#directory, `${++this.#count}.mjs`);
|
|
1205
|
+
const { outputFiles } = await build({
|
|
1206
|
+
...this.#options(file),
|
|
1207
|
+
outfile,
|
|
1208
|
+
sourcemap: "inline",
|
|
1209
|
+
sourcesContent: false,
|
|
1210
|
+
// for coverage, code no test imports counts too
|
|
1211
|
+
treeShaking: !this.#keep
|
|
1212
|
+
});
|
|
1213
|
+
const code = outputFiles[0]?.text ?? "";
|
|
1214
|
+
writeFileSync2(outfile, code);
|
|
1215
|
+
if (this.#keep) this.bundles.set(outfile, code);
|
|
1216
|
+
return pathToFileURL2(outfile).href;
|
|
1217
|
+
}
|
|
1218
|
+
/** The absolute paths of the local files a test file is bundled from, itself included. */
|
|
1219
|
+
async inputs(file) {
|
|
1220
|
+
const { metafile } = await build({
|
|
1221
|
+
...this.#options(file),
|
|
1222
|
+
metafile: true
|
|
1223
|
+
});
|
|
1224
|
+
return Object.keys(metafile.inputs).filter((input) => !/^[\w-]+:/.test(input)).map((input) => resolve(input)).filter((input) => input !== this.#globals);
|
|
1225
|
+
}
|
|
1226
|
+
/** What compiling and listing the inputs share. */
|
|
1227
|
+
#options(file) {
|
|
1228
|
+
return {
|
|
1229
|
+
entryPoints: [file],
|
|
1230
|
+
write: false,
|
|
1231
|
+
bundle: true,
|
|
1232
|
+
platform: "node",
|
|
1233
|
+
format: "esm",
|
|
1234
|
+
target: `node${process.versions.node.split(".")[0]}`,
|
|
1235
|
+
inject: [this.#globals],
|
|
1236
|
+
// defines replace only unbound names, so a file's own `__dirname` stays
|
|
1237
|
+
define: Object.fromEntries([
|
|
1238
|
+
..._Transformer.#meta.map((name) => [
|
|
1239
|
+
`import.meta.${name}`,
|
|
1240
|
+
`__testero.${name}`
|
|
1241
|
+
]),
|
|
1242
|
+
...Object.entries(_Transformer.#commonjs).map(
|
|
1243
|
+
([name, property2]) => [name, `__testero.${property2}`]
|
|
869
1244
|
)
|
|
870
|
-
)
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1245
|
+
]),
|
|
1246
|
+
// CommonJS code in the bundle may require installed packages
|
|
1247
|
+
banner: {
|
|
1248
|
+
js: 'import { createRequire as __testeroRequire } from "node:module";\nconst require = __testeroRequire(import.meta.url);'
|
|
1249
|
+
},
|
|
1250
|
+
logLevel: "silent",
|
|
1251
|
+
plugins: [this.#plugin()]
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
/** The esbuild loader of a source file, undefined for files it does not compile. */
|
|
1255
|
+
static loader(path) {
|
|
1256
|
+
return _Transformer.#loaders[extname(path)];
|
|
1257
|
+
}
|
|
1258
|
+
/** Removes the compiled files. */
|
|
1259
|
+
dispose() {
|
|
1260
|
+
rmSync(this.#directory, { recursive: true, force: true });
|
|
1261
|
+
}
|
|
1262
|
+
#plugin() {
|
|
1263
|
+
return {
|
|
1264
|
+
name: "testero",
|
|
1265
|
+
setup: /* @__PURE__ */ __name((build2) => {
|
|
1266
|
+
build2.onResolve({ filter: /^testero-meta:/ }, (args) => ({
|
|
1267
|
+
path: args.path.slice("testero-meta:".length),
|
|
1268
|
+
namespace: "testero-meta"
|
|
1269
|
+
}));
|
|
1270
|
+
build2.onLoad(
|
|
1271
|
+
{ filter: /.*/, namespace: "testero-meta" },
|
|
1272
|
+
(args) => ({
|
|
1273
|
+
contents: `export default ${JSON.stringify({
|
|
1274
|
+
url: pathToFileURL2(args.path).href,
|
|
1275
|
+
filename: args.path,
|
|
1276
|
+
dirname: dirname(args.path)
|
|
1277
|
+
})};`,
|
|
1278
|
+
loader: "js"
|
|
1279
|
+
})
|
|
1280
|
+
);
|
|
1281
|
+
build2.onResolve({ filter: /^[^./]/ }, async (args) => {
|
|
1282
|
+
if (args.pluginData === _Transformer.#own) return void 0;
|
|
1283
|
+
if (_Transformer.modules.includes(args.path))
|
|
1284
|
+
return { path: this.#globals };
|
|
1285
|
+
const resolved = await build2.resolve(args.path, {
|
|
1286
|
+
kind: args.kind,
|
|
1287
|
+
resolveDir: args.resolveDir,
|
|
1288
|
+
pluginData: _Transformer.#own
|
|
1289
|
+
});
|
|
1290
|
+
if (resolved.errors.length > 0)
|
|
1291
|
+
return { errors: resolved.errors };
|
|
1292
|
+
if (resolved.external)
|
|
1293
|
+
return { path: args.path, external: true };
|
|
1294
|
+
if (!_Transformer.#installed(resolved.path))
|
|
1295
|
+
return { path: resolved.path };
|
|
1296
|
+
return {
|
|
1297
|
+
path: args.kind === "require-call" ? resolved.path : pathToFileURL2(resolved.path).href,
|
|
1298
|
+
external: true
|
|
1299
|
+
};
|
|
1300
|
+
});
|
|
1301
|
+
build2.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
|
|
1302
|
+
if (_Transformer.#installed(args.path)) return void 0;
|
|
1303
|
+
const source = readFileSync2(args.path, "utf8");
|
|
1304
|
+
return {
|
|
1305
|
+
contents: _Transformer.#locate(source, args.path),
|
|
1306
|
+
loader: _Transformer.#loaders[extname(args.path)] ?? "js",
|
|
1307
|
+
resolveDir: dirname(args.path)
|
|
1308
|
+
};
|
|
1309
|
+
});
|
|
1310
|
+
}, "setup")
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* A source with `__testero` describing its file, when it refers to it.
|
|
1315
|
+
* Modules import it after their last line, as imports are hoisted.
|
|
1316
|
+
* CommonJS files, without import or export statements, require it at the
|
|
1317
|
+
* start of their first line instead, as an import would make them modules
|
|
1318
|
+
* and their `module.exports` would be lost.
|
|
1319
|
+
*/
|
|
1320
|
+
static #locate(source, path) {
|
|
1321
|
+
if (!_Transformer.#located.test(source)) return source;
|
|
1322
|
+
const meta = JSON.stringify(`testero-meta:${path}`);
|
|
1323
|
+
if (/\.m[jt]s$/.test(path) || /^\s*(?:import\s*[\w{*"']|export\b)/m.test(source))
|
|
1324
|
+
return `${source}
|
|
1325
|
+
import __testero from ${meta};
|
|
1326
|
+
`;
|
|
1327
|
+
const start = source.startsWith("#!") ? source.indexOf("\n") + 1 : 0;
|
|
1328
|
+
if (source.startsWith("#!") && start === 0) return source;
|
|
1329
|
+
return `${source.slice(0, start)}var __testero = require(${meta}).default;${source.slice(start)}`;
|
|
1330
|
+
}
|
|
1331
|
+
static #installed(path) {
|
|
1332
|
+
return path.split(sep).includes("node_modules");
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
// packages/testero/src/coverage.ts
|
|
1339
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1340
|
+
import { Session } from "node:inspector/promises";
|
|
1341
|
+
import { dirname as dirname2, join as join3, relative, resolve as resolve2 } from "node:path";
|
|
1342
|
+
import { fileURLToPath } from "node:url";
|
|
1343
|
+
import { transform } from "esbuild";
|
|
1344
|
+
var Coverage;
|
|
1345
|
+
var init_coverage = __esm({
|
|
1346
|
+
"packages/testero/src/coverage.ts"() {
|
|
1347
|
+
"use strict";
|
|
1348
|
+
init_transformer();
|
|
1349
|
+
Coverage = class _Coverage {
|
|
1350
|
+
static {
|
|
1351
|
+
__name(this, "Coverage");
|
|
1352
|
+
}
|
|
1353
|
+
static #digits = (() => {
|
|
1354
|
+
const digits = new Int8Array(128).fill(-1);
|
|
1355
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1356
|
+
for (let index = 0; index < alphabet.length; index++)
|
|
1357
|
+
digits[alphabet.charCodeAt(index)] = index;
|
|
1358
|
+
return digits;
|
|
1359
|
+
})();
|
|
1360
|
+
static #inline = /\/\/# sourceMappingURL=data:application\/json;base64,([\w+/=]+)\s*$/;
|
|
1361
|
+
#root;
|
|
1362
|
+
#tracks;
|
|
1363
|
+
/** What runs within this one took, which reset the counts of this one. */
|
|
1364
|
+
#taken = [];
|
|
1365
|
+
#receive = /* @__PURE__ */ __name((result) => this.#taken.push(result), "#receive");
|
|
1366
|
+
/** Reports the files, relative to the root, that `tracks` takes. */
|
|
1367
|
+
constructor(root, tracks) {
|
|
1368
|
+
this.#root = root;
|
|
1369
|
+
this.#tracks = tracks;
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* The runs measuring coverage, a run within a run, like testero testing
|
|
1373
|
+
* itself, among them, and their session. Starting coverage clears the
|
|
1374
|
+
* counts of calls and taking it resets all counts, of the whole process,
|
|
1375
|
+
* so the runs share one session, started once, and each gets what any
|
|
1376
|
+
* of the others takes. Kept on globalThis, as nested runs may come from
|
|
1377
|
+
* different copies of this module.
|
|
1378
|
+
*/
|
|
1379
|
+
static #shared() {
|
|
1380
|
+
const key = /* @__PURE__ */ Symbol.for("@aljazmc/testero.coverage");
|
|
1381
|
+
const global = globalThis;
|
|
1382
|
+
global[key] ??= { session: void 0, runs: /* @__PURE__ */ new Set() };
|
|
1383
|
+
return global[key];
|
|
1384
|
+
}
|
|
1385
|
+
/** Starts counting, before the tests load. */
|
|
1386
|
+
async start() {
|
|
1387
|
+
const shared = _Coverage.#shared();
|
|
1388
|
+
if (!shared.session) {
|
|
1389
|
+
const session = new Session();
|
|
1390
|
+
session.connect();
|
|
1391
|
+
await session.post("Profiler.enable");
|
|
1392
|
+
await session.post("Profiler.startPreciseCoverage", {
|
|
1393
|
+
callCount: true,
|
|
1394
|
+
detailed: true
|
|
1395
|
+
});
|
|
1396
|
+
shared.session = session;
|
|
1397
|
+
}
|
|
1398
|
+
shared.runs.add(this.#receive);
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* The coverage of the tracked sources of the bundles, by their paths,
|
|
1402
|
+
* and of the files no test loaded, sorted by path.
|
|
1403
|
+
*/
|
|
1404
|
+
async collect(bundles, unloaded = []) {
|
|
1405
|
+
const shared = _Coverage.#shared();
|
|
1406
|
+
const session = shared.session;
|
|
1407
|
+
if (!session || !shared.runs.delete(this.#receive))
|
|
1408
|
+
throw new Error("Coverage was not started.");
|
|
1409
|
+
const { result } = await session.post(
|
|
1410
|
+
"Profiler.takePreciseCoverage"
|
|
1411
|
+
);
|
|
1412
|
+
for (const receive of shared.runs) receive(result);
|
|
1413
|
+
if (shared.runs.size === 0) {
|
|
1414
|
+
session.disconnect();
|
|
1415
|
+
shared.session = void 0;
|
|
1416
|
+
}
|
|
1417
|
+
const files = /* @__PURE__ */ new Map();
|
|
1418
|
+
for (const taken of [...this.#taken, result])
|
|
1419
|
+
for (const script of taken) {
|
|
1420
|
+
const path = script.url.startsWith("file:") ? fileURLToPath(script.url) : script.url;
|
|
1421
|
+
const code = bundles.get(path);
|
|
1422
|
+
if (code !== void 0)
|
|
1423
|
+
this.#bundle(code, dirname2(path), script.functions, files);
|
|
1424
|
+
}
|
|
1425
|
+
for (const file of unloaded)
|
|
1426
|
+
if (!files.has(file)) files.set(file, await this.#unloaded(file));
|
|
1427
|
+
return [...files.values()].sort(
|
|
1428
|
+
(a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
/** Adds the counts of the sources of one bundle. */
|
|
1432
|
+
#bundle(code, directory, functions, files) {
|
|
1433
|
+
const inline = _Coverage.#inline.exec(code)?.[1];
|
|
1434
|
+
if (inline === void 0) return;
|
|
1435
|
+
const map = JSON.parse(Buffer.from(inline, "base64").toString());
|
|
1436
|
+
const sources = map.sources.map((source) => {
|
|
1437
|
+
const file = relative(this.#root, resolve2(directory, source)).split("\\").join("/");
|
|
1438
|
+
if (!this.#tracks(file)) return void 0;
|
|
1439
|
+
let coverage = files.get(file);
|
|
1440
|
+
if (!coverage) {
|
|
1441
|
+
coverage = {
|
|
1442
|
+
file,
|
|
1443
|
+
lines: /* @__PURE__ */ new Map(),
|
|
1444
|
+
functions: /* @__PURE__ */ new Map(),
|
|
1445
|
+
loaded: true
|
|
1446
|
+
};
|
|
1447
|
+
files.set(file, coverage);
|
|
1448
|
+
}
|
|
1449
|
+
return coverage;
|
|
1450
|
+
});
|
|
1451
|
+
if (sources.every((source) => source === void 0)) return;
|
|
1452
|
+
const counts = new Float64Array(code.length);
|
|
1453
|
+
const ranges = functions.flatMap((fn) => fn.ranges).sort(
|
|
1454
|
+
(a, b) => b.endOffset - b.startOffset - (a.endOffset - a.startOffset)
|
|
1455
|
+
);
|
|
1456
|
+
for (const range of ranges)
|
|
1457
|
+
counts.fill(range.count, range.startOffset, range.endOffset);
|
|
1458
|
+
const starts = [0];
|
|
1459
|
+
for (let index = 0; index < code.length; index++)
|
|
1460
|
+
if (code.charCodeAt(index) === 10) starts.push(index + 1);
|
|
1461
|
+
const segments = _Coverage.decode(map.mappings);
|
|
1462
|
+
const offsets = segments.map(
|
|
1463
|
+
(segment) => (starts[segment.line] ?? code.length) + segment.column
|
|
1464
|
+
);
|
|
1465
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1466
|
+
segments.forEach((segment, index) => {
|
|
1467
|
+
const coverage = sources[segment.source];
|
|
1468
|
+
if (!coverage) return;
|
|
1469
|
+
const next = segments[index + 1];
|
|
1470
|
+
const end = next?.line === segment.line ? offsets[index + 1] : starts[segment.line + 1] ?? code.length;
|
|
1471
|
+
let offset = offsets[index];
|
|
1472
|
+
while (offset < end && /\s/.test(code[offset])) offset++;
|
|
1473
|
+
if (offset >= end) return;
|
|
1474
|
+
const counted = lines.get(coverage) ?? /* @__PURE__ */ new Map();
|
|
1475
|
+
lines.set(coverage, counted);
|
|
1476
|
+
const line = segment.sourceLine + 1;
|
|
1477
|
+
const count = counts[offset] ?? 0;
|
|
1478
|
+
counted.set(line, Math.max(counted.get(line) ?? 0, count));
|
|
1479
|
+
});
|
|
1480
|
+
for (const [coverage, counted] of lines)
|
|
1481
|
+
for (const [line, count] of counted)
|
|
1482
|
+
coverage.lines.set(
|
|
1483
|
+
line,
|
|
1484
|
+
(coverage.lines.get(line) ?? 0) + count
|
|
1485
|
+
);
|
|
1486
|
+
for (const { functionName, ranges: ranges2 } of functions) {
|
|
1487
|
+
const range = ranges2[0];
|
|
1488
|
+
if (!range || range.startOffset === 0 || functionName.startsWith("<"))
|
|
1489
|
+
continue;
|
|
1490
|
+
const index = _Coverage.#before(offsets, range.startOffset) ?? -1;
|
|
1491
|
+
const segment = segments[index];
|
|
1492
|
+
const line = starts[_Coverage.#before(starts, range.startOffset) ?? 0];
|
|
1493
|
+
if (!segment || offsets[index] < (line ?? 0)) continue;
|
|
1494
|
+
const coverage = sources[segment.source];
|
|
1495
|
+
if (!coverage) continue;
|
|
1496
|
+
const key = `${segment.sourceLine + 1}:${segment.sourceColumn + 1}`;
|
|
1497
|
+
const known = coverage.functions.get(key);
|
|
1498
|
+
if (known) known.count += range.count;
|
|
1499
|
+
else
|
|
1500
|
+
coverage.functions.set(key, {
|
|
1501
|
+
name: functionName,
|
|
1502
|
+
line: segment.sourceLine + 1,
|
|
1503
|
+
count: range.count
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
/** A file no test loaded: its lines with code, none of them run. */
|
|
1508
|
+
async #unloaded(file) {
|
|
1509
|
+
const coverage = {
|
|
1510
|
+
file,
|
|
1511
|
+
lines: /* @__PURE__ */ new Map(),
|
|
1512
|
+
functions: /* @__PURE__ */ new Map(),
|
|
1513
|
+
loaded: false
|
|
1514
|
+
};
|
|
1515
|
+
const path = join3(this.#root, file);
|
|
1516
|
+
const loader = Transformer.loader(path);
|
|
1517
|
+
if (!loader) return coverage;
|
|
1518
|
+
try {
|
|
1519
|
+
const { map } = await transform(readFileSync3(path, "utf8"), {
|
|
1520
|
+
loader,
|
|
1521
|
+
sourcemap: "external",
|
|
1522
|
+
sourcefile: file
|
|
1523
|
+
});
|
|
1524
|
+
const { mappings } = JSON.parse(map);
|
|
1525
|
+
for (const segment of _Coverage.decode(mappings))
|
|
1526
|
+
coverage.lines.set(segment.sourceLine + 1, 0);
|
|
1527
|
+
} catch {
|
|
1528
|
+
}
|
|
1529
|
+
return coverage;
|
|
1530
|
+
}
|
|
1531
|
+
/** The index of the last of the sorted numbers at or before a number. */
|
|
1532
|
+
static #before(sorted, value) {
|
|
1533
|
+
let low = 0;
|
|
1534
|
+
let high = sorted.length - 1;
|
|
1535
|
+
let found;
|
|
1536
|
+
while (low <= high) {
|
|
1537
|
+
const middle = low + high >> 1;
|
|
1538
|
+
if (sorted[middle] <= value) {
|
|
1539
|
+
found = middle;
|
|
1540
|
+
low = middle + 1;
|
|
1541
|
+
} else high = middle - 1;
|
|
1542
|
+
}
|
|
1543
|
+
return found;
|
|
1544
|
+
}
|
|
1545
|
+
/** The mappings of a source map with a source, in order, its base64 VLQs decoded. */
|
|
1546
|
+
static decode(mappings) {
|
|
1547
|
+
const segments = [];
|
|
1548
|
+
let line = 0;
|
|
1549
|
+
let column = 0;
|
|
1550
|
+
let source = 0;
|
|
1551
|
+
let sourceLine = 0;
|
|
1552
|
+
let sourceColumn = 0;
|
|
1553
|
+
let fields = [];
|
|
1554
|
+
let value = 0;
|
|
1555
|
+
let shift = 0;
|
|
1556
|
+
for (let index = 0; index <= mappings.length; index++) {
|
|
1557
|
+
const code = mappings.charCodeAt(index);
|
|
1558
|
+
if (Number.isNaN(code) || code === 44 || code === 59) {
|
|
1559
|
+
if (fields.length > 0) {
|
|
1560
|
+
column += fields[0];
|
|
1561
|
+
if (fields.length >= 4) {
|
|
1562
|
+
source += fields[1];
|
|
1563
|
+
sourceLine += fields[2];
|
|
1564
|
+
sourceColumn += fields[3];
|
|
1565
|
+
segments.push({
|
|
1566
|
+
line,
|
|
1567
|
+
column,
|
|
1568
|
+
source,
|
|
1569
|
+
sourceLine,
|
|
1570
|
+
sourceColumn
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
fields = [];
|
|
1574
|
+
}
|
|
1575
|
+
if (code === 59) {
|
|
1576
|
+
line++;
|
|
1577
|
+
column = 0;
|
|
1578
|
+
}
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
const digit = _Coverage.#digits[code] ?? -1;
|
|
1582
|
+
if (digit < 0)
|
|
1583
|
+
throw new Error(
|
|
1584
|
+
`The source map has "${mappings[index]}" in its mappings.`
|
|
1585
|
+
);
|
|
1586
|
+
value += (digit & 31) * 2 ** shift;
|
|
1587
|
+
if (digit & 32) shift += 5;
|
|
1588
|
+
else {
|
|
1589
|
+
const magnitude = Math.floor(value / 2);
|
|
1590
|
+
fields.push(value % 2 === 1 ? -magnitude : magnitude);
|
|
1591
|
+
value = 0;
|
|
1592
|
+
shift = 0;
|
|
882
1593
|
}
|
|
883
1594
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1595
|
+
return segments;
|
|
1596
|
+
}
|
|
1597
|
+
/** How many of the lines with code ran. */
|
|
1598
|
+
static lines(coverage) {
|
|
1599
|
+
const counts = [...coverage.lines.values()];
|
|
1600
|
+
return {
|
|
1601
|
+
covered: counts.filter((count) => count > 0).length,
|
|
1602
|
+
total: counts.length
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
/** How many of the functions ran. */
|
|
1606
|
+
static functions(coverage) {
|
|
1607
|
+
const counts = [...coverage.functions.values()];
|
|
1608
|
+
return {
|
|
1609
|
+
covered: counts.filter(({ count }) => count > 0).length,
|
|
1610
|
+
total: counts.length
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
/** The sums of ratios. */
|
|
1614
|
+
static sum(ratios) {
|
|
1615
|
+
return ratios.reduce(
|
|
1616
|
+
(sum, ratio) => ({
|
|
1617
|
+
covered: sum.covered + ratio.covered,
|
|
1618
|
+
total: sum.total + ratio.total
|
|
1619
|
+
}),
|
|
1620
|
+
{ covered: 0, total: 0 }
|
|
896
1621
|
);
|
|
897
1622
|
}
|
|
898
|
-
/**
|
|
899
|
-
static
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
];
|
|
907
|
-
|
|
1623
|
+
/** A percentage like Jest's, 100 when there is nothing to run, unless nothing loaded it. */
|
|
1624
|
+
static percent(ratio, loaded = true) {
|
|
1625
|
+
if (ratio.total === 0) return loaded ? 100 : 0;
|
|
1626
|
+
return Math.floor(ratio.covered / ratio.total * 1e4) / 100;
|
|
1627
|
+
}
|
|
1628
|
+
/** The lines that did not run, in ranges like `3-5,9`. */
|
|
1629
|
+
static uncovered(coverage) {
|
|
1630
|
+
const lines = [...coverage.lines].filter(([, count]) => count === 0).map(([line]) => line).sort((a, b) => a - b);
|
|
1631
|
+
const ranges = [];
|
|
1632
|
+
for (let index = 0; index < lines.length; ) {
|
|
1633
|
+
const first = lines[index];
|
|
1634
|
+
let last = first;
|
|
1635
|
+
while (lines[index + 1] === last + 1)
|
|
1636
|
+
last = lines[++index];
|
|
1637
|
+
ranges.push(first === last ? `${first}` : `${first}-${last}`);
|
|
1638
|
+
index++;
|
|
1639
|
+
}
|
|
1640
|
+
return ranges.join(",");
|
|
1641
|
+
}
|
|
1642
|
+
/** The coverage in the lcov format editors and services read. */
|
|
1643
|
+
static lcov(root, files) {
|
|
1644
|
+
return files.map((coverage) => {
|
|
1645
|
+
const records = ["TN:", `SF:${join3(root, coverage.file)}`];
|
|
1646
|
+
const functions = [...coverage.functions.values()].sort(
|
|
1647
|
+
(a, b) => a.line - b.line
|
|
1648
|
+
);
|
|
1649
|
+
const names = /* @__PURE__ */ new Map();
|
|
1650
|
+
const named = functions.map((fn) => {
|
|
1651
|
+
const base = fn.name || "(anonymous)";
|
|
1652
|
+
const seen = names.get(base) ?? 0;
|
|
1653
|
+
names.set(base, seen + 1);
|
|
1654
|
+
return {
|
|
1655
|
+
...fn,
|
|
1656
|
+
name: seen === 0 ? base : `${base}_${seen}`
|
|
1657
|
+
};
|
|
1658
|
+
});
|
|
1659
|
+
for (const fn of named)
|
|
1660
|
+
records.push(`FN:${fn.line},${fn.name}`);
|
|
1661
|
+
for (const fn of named)
|
|
1662
|
+
records.push(`FNDA:${fn.count},${fn.name}`);
|
|
1663
|
+
const ratio = _Coverage.functions(coverage);
|
|
1664
|
+
records.push(`FNF:${ratio.total}`, `FNH:${ratio.covered}`);
|
|
1665
|
+
for (const [line, count] of [...coverage.lines].sort(
|
|
1666
|
+
([a], [b]) => a - b
|
|
1667
|
+
))
|
|
1668
|
+
records.push(`DA:${line},${count}`);
|
|
1669
|
+
const lines = _Coverage.lines(coverage);
|
|
1670
|
+
records.push(
|
|
1671
|
+
`LF:${lines.total}`,
|
|
1672
|
+
`LH:${lines.covered}`,
|
|
1673
|
+
"end_of_record"
|
|
1674
|
+
);
|
|
1675
|
+
return `${records.join("\n")}
|
|
1676
|
+
`;
|
|
1677
|
+
}).join("");
|
|
908
1678
|
}
|
|
909
1679
|
};
|
|
910
1680
|
}
|
|
@@ -1375,46 +2145,47 @@ var init_assertion = __esm({
|
|
|
1375
2145
|
}
|
|
1376
2146
|
/** The same assertion, expecting the opposite. */
|
|
1377
2147
|
get not() {
|
|
1378
|
-
return
|
|
1379
|
-
...this.#context,
|
|
1380
|
-
isNot: !this.#context.isNot
|
|
1381
|
-
});
|
|
2148
|
+
return this.#with({ isNot: !this.#context.isNot });
|
|
1382
2149
|
}
|
|
1383
2150
|
/** The assertion on what a promise resolves to, its matchers return promises. */
|
|
1384
2151
|
get resolves() {
|
|
1385
|
-
return
|
|
1386
|
-
...this.#context,
|
|
1387
|
-
promise: "resolves"
|
|
1388
|
-
});
|
|
2152
|
+
return this.#with({ promise: "resolves" });
|
|
1389
2153
|
}
|
|
1390
2154
|
/** The assertion on why a promise rejects, its matchers return promises. */
|
|
1391
2155
|
get rejects() {
|
|
1392
|
-
return
|
|
2156
|
+
return this.#with({ promise: "rejects" });
|
|
2157
|
+
}
|
|
2158
|
+
/** The same received value with another context, of the same class to keep its matchers. */
|
|
2159
|
+
#with(change) {
|
|
2160
|
+
const Same = this.constructor;
|
|
2161
|
+
return new Same(this.#received, {
|
|
1393
2162
|
...this.#context,
|
|
1394
|
-
|
|
2163
|
+
...change
|
|
1395
2164
|
});
|
|
1396
2165
|
}
|
|
1397
|
-
/**
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
2166
|
+
/**
|
|
2167
|
+
* Adds a matcher to the assertions of a class: Assertion itself for the
|
|
2168
|
+
* built in ones, the class of one test file for `expect.extend`.
|
|
2169
|
+
*/
|
|
2170
|
+
static define(target, name, matcher) {
|
|
2171
|
+
Object.defineProperty(target.prototype, name, {
|
|
1401
2172
|
configurable: true,
|
|
1402
2173
|
writable: true,
|
|
1403
2174
|
value: /* @__PURE__ */ __name(function check(...args) {
|
|
1404
|
-
return this.#run(name, args, check);
|
|
2175
|
+
return this.#run(name, matcher, args, check);
|
|
1405
2176
|
}, "check")
|
|
1406
2177
|
});
|
|
1407
2178
|
}
|
|
1408
2179
|
/** Runs a matcher now, or once the promise settles for `.resolves` and `.rejects`. */
|
|
1409
|
-
#run(name, args, caller) {
|
|
2180
|
+
#run(name, matcher, args, caller) {
|
|
1410
2181
|
_Assertion.count++;
|
|
1411
2182
|
const origin = {};
|
|
1412
2183
|
Error.captureStackTrace(origin, caller);
|
|
1413
2184
|
const { promise } = this.#context;
|
|
1414
|
-
if (!promise) return this.#check(
|
|
2185
|
+
if (!promise) return this.#check(matcher, this.#received, args, origin);
|
|
1415
2186
|
const settled = /* @__PURE__ */ __name((value, resolved) => {
|
|
1416
2187
|
if (resolved === (promise === "resolves"))
|
|
1417
|
-
return this.#check(
|
|
2188
|
+
return this.#check(matcher, value, args, origin);
|
|
1418
2189
|
throw _Assertion.#error(
|
|
1419
2190
|
`expect(received).${promise}.${name}()
|
|
1420
2191
|
|
|
@@ -1435,19 +2206,34 @@ Matcher error: received value must be a promise`,
|
|
|
1435
2206
|
(reason) => settled(reason, false)
|
|
1436
2207
|
);
|
|
1437
2208
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
2209
|
+
/** Calls a matcher, and fails when it does not pass, after awaiting an async one. */
|
|
2210
|
+
#check(matcher, received, args, origin) {
|
|
2211
|
+
const failed = /* @__PURE__ */ __name((error) => _Assertion.#error(
|
|
2212
|
+
error instanceof Error ? error.message : String(error),
|
|
2213
|
+
origin
|
|
2214
|
+
), "failed");
|
|
1440
2215
|
let result;
|
|
1441
2216
|
try {
|
|
1442
2217
|
result = matcher.call(this.#context, received, ...args);
|
|
1443
2218
|
} catch (error) {
|
|
2219
|
+
throw failed(error);
|
|
2220
|
+
}
|
|
2221
|
+
if (typeof result?.then === "function")
|
|
2222
|
+
return Promise.resolve(result).then(
|
|
2223
|
+
(settled) => this.#verdict(settled, origin),
|
|
2224
|
+
(error) => {
|
|
2225
|
+
throw failed(error);
|
|
2226
|
+
}
|
|
2227
|
+
);
|
|
2228
|
+
return this.#verdict(result, origin);
|
|
2229
|
+
}
|
|
2230
|
+
/** Throws when a matcher result is not what the assertion expects. */
|
|
2231
|
+
#verdict(result, origin) {
|
|
2232
|
+
if (Boolean(result?.pass) === this.#context.isNot)
|
|
1444
2233
|
throw _Assertion.#error(
|
|
1445
|
-
|
|
2234
|
+
typeof result?.message === "function" ? String(result.message()) : "The matcher did not return a result with pass and message.",
|
|
1446
2235
|
origin
|
|
1447
2236
|
);
|
|
1448
|
-
}
|
|
1449
|
-
if (result.pass === this.#context.isNot)
|
|
1450
|
-
throw _Assertion.#error(result.message(), origin);
|
|
1451
2237
|
}
|
|
1452
2238
|
static #error(message, origin) {
|
|
1453
2239
|
const error = new AssertionError(message);
|
|
@@ -1458,7 +2244,7 @@ ${frames}` : ""}`;
|
|
|
1458
2244
|
}
|
|
1459
2245
|
};
|
|
1460
2246
|
for (const [name, matcher] of Object.entries(matchers))
|
|
1461
|
-
Assertion.define(name, matcher);
|
|
2247
|
+
Assertion.define(Assertion, name, matcher);
|
|
1462
2248
|
}
|
|
1463
2249
|
});
|
|
1464
2250
|
|
|
@@ -1588,307 +2374,96 @@ var init_asymmetric = __esm({
|
|
|
1588
2374
|
return Math.abs(this.sample - other) < 10 ** -this.#digits / 2;
|
|
1589
2375
|
}
|
|
1590
2376
|
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"() {
|
|
2377
|
+
return `Number${this.prefix}CloseTo ${this.sample} (${this.#digits} digits)`;
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
// packages/testero/src/expect/expect.ts
|
|
2384
|
+
var negatable, Expect;
|
|
2385
|
+
var init_expect = __esm({
|
|
2386
|
+
"packages/testero/src/expect/expect.ts"() {
|
|
1759
2387
|
"use strict";
|
|
1760
|
-
|
|
1761
|
-
|
|
2388
|
+
init_assertion();
|
|
2389
|
+
init_asymmetric();
|
|
2390
|
+
negatable = /* @__PURE__ */ __name((inverse) => ({
|
|
2391
|
+
objectContaining: /* @__PURE__ */ __name((object) => new ObjectContaining(object, inverse), "objectContaining"),
|
|
2392
|
+
arrayContaining: /* @__PURE__ */ __name((array) => new ArrayContaining(array, inverse), "arrayContaining"),
|
|
2393
|
+
stringContaining: /* @__PURE__ */ __name((text) => new StringContaining(text, inverse), "stringContaining"),
|
|
2394
|
+
stringMatching: /* @__PURE__ */ __name((pattern) => new StringMatching(new RegExp(pattern), inverse), "stringMatching"),
|
|
2395
|
+
closeTo: /* @__PURE__ */ __name((number, digits) => new CloseTo(number, digits, inverse), "closeTo")
|
|
2396
|
+
}), "negatable");
|
|
2397
|
+
Expect = class {
|
|
1762
2398
|
static {
|
|
1763
|
-
__name(this, "
|
|
2399
|
+
__name(this, "Expect");
|
|
1764
2400
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
".ts": "ts",
|
|
1773
|
-
".cts": "ts",
|
|
1774
|
-
".mts": "ts",
|
|
1775
|
-
".tsx": "tsx"
|
|
2401
|
+
#expected;
|
|
2402
|
+
#needed = false;
|
|
2403
|
+
/** The assertions of this test file, `expect.extend` adds matchers to them only. */
|
|
2404
|
+
#Assertion = class extends Assertion {
|
|
2405
|
+
static {
|
|
2406
|
+
__name(this, "#Assertion");
|
|
2407
|
+
}
|
|
1776
2408
|
};
|
|
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 });
|
|
2409
|
+
/** Starts the counts of a new test. */
|
|
2410
|
+
reset() {
|
|
2411
|
+
Assertion.count = 0;
|
|
2412
|
+
this.#expected = void 0;
|
|
2413
|
+
this.#needed = false;
|
|
1822
2414
|
}
|
|
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
|
-
};
|
|
2415
|
+
/** Failures of `expect.assertions(n)` and `expect.hasAssertions()` after a test. */
|
|
2416
|
+
verify() {
|
|
2417
|
+
const count = Assertion.count;
|
|
2418
|
+
if (this.#expected !== void 0 && count !== this.#expected)
|
|
2419
|
+
return [
|
|
2420
|
+
new AssertionError(
|
|
2421
|
+
`expect.assertions(${this.#expected})
|
|
2422
|
+
|
|
2423
|
+
Expected ${this.#expected} assertion${this.#expected === 1 ? "" : "s"} to be called but received ${count} assertion call${count === 1 ? "" : "s"}.`
|
|
2424
|
+
)
|
|
2425
|
+
];
|
|
2426
|
+
if (this.#needed && count === 0)
|
|
2427
|
+
return [
|
|
2428
|
+
new AssertionError(
|
|
2429
|
+
"expect.hasAssertions()\n\nExpected at least one assertion to be called but received none."
|
|
2430
|
+
)
|
|
2431
|
+
];
|
|
2432
|
+
return [];
|
|
1876
2433
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
2434
|
+
/** The function test files call as `expect`. */
|
|
2435
|
+
create() {
|
|
2436
|
+
const expect = /* @__PURE__ */ __name((received) => new this.#Assertion(received), "expect");
|
|
2437
|
+
return Object.assign(expect, {
|
|
2438
|
+
...negatable(false),
|
|
2439
|
+
not: negatable(true),
|
|
2440
|
+
any: /* @__PURE__ */ __name((type) => new Any(type), "any"),
|
|
2441
|
+
anything: /* @__PURE__ */ __name(() => new Anything(void 0), "anything"),
|
|
2442
|
+
assertions: /* @__PURE__ */ __name((count) => {
|
|
2443
|
+
this.#expected = count;
|
|
2444
|
+
}, "assertions"),
|
|
2445
|
+
hasAssertions: /* @__PURE__ */ __name(() => {
|
|
2446
|
+
this.#needed = true;
|
|
2447
|
+
}, "hasAssertions"),
|
|
2448
|
+
extend: /* @__PURE__ */ __name((added) => {
|
|
2449
|
+
for (const [name, matcher] of Object.entries(added))
|
|
2450
|
+
Assertion.define(this.#Assertion, name, matcher);
|
|
2451
|
+
}, "extend")
|
|
2452
|
+
});
|
|
1879
2453
|
}
|
|
1880
2454
|
};
|
|
1881
2455
|
}
|
|
1882
2456
|
});
|
|
1883
2457
|
|
|
1884
2458
|
// packages/testero/src/reporter.ts
|
|
1885
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1886
|
-
import { dirname as
|
|
1887
|
-
import { fileURLToPath } from "node:url";
|
|
2459
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
2460
|
+
import { dirname as dirname3, isAbsolute, relative as relative2 } from "node:path";
|
|
2461
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1888
2462
|
var Reporter;
|
|
1889
2463
|
var init_reporter = __esm({
|
|
1890
2464
|
"packages/testero/src/reporter.ts"() {
|
|
1891
2465
|
"use strict";
|
|
2466
|
+
init_coverage();
|
|
1892
2467
|
init_assertion();
|
|
1893
2468
|
init_format();
|
|
1894
2469
|
init_transformer();
|
|
@@ -1907,7 +2482,7 @@ var init_reporter = __esm({
|
|
|
1907
2482
|
gray: 90
|
|
1908
2483
|
};
|
|
1909
2484
|
/** Stack frames from testero itself are left out of failures. */
|
|
1910
|
-
static #own =
|
|
2485
|
+
static #own = dirname3(fileURLToPath2(import.meta.url));
|
|
1911
2486
|
#root;
|
|
1912
2487
|
#verbose;
|
|
1913
2488
|
#color;
|
|
@@ -1985,6 +2560,66 @@ ${failed ? "\n" : ""}`);
|
|
|
1985
2560
|
].join("\n")
|
|
1986
2561
|
);
|
|
1987
2562
|
}
|
|
2563
|
+
/** The coverage of each file in a table like Jest's, under the totals. */
|
|
2564
|
+
coverage(files) {
|
|
2565
|
+
const row = /* @__PURE__ */ __name((name, functions, lines, uncovered) => [
|
|
2566
|
+
name,
|
|
2567
|
+
String(functions),
|
|
2568
|
+
String(lines),
|
|
2569
|
+
uncovered.length > 40 ? `${uncovered.slice(0, 37)}...` : uncovered
|
|
2570
|
+
], "row");
|
|
2571
|
+
const header = ["File", "% Funcs", "% Lines", "Uncovered Line #s"];
|
|
2572
|
+
const rows = [
|
|
2573
|
+
row(
|
|
2574
|
+
"All files",
|
|
2575
|
+
Coverage.percent(Coverage.sum(files.map(Coverage.functions))),
|
|
2576
|
+
Coverage.percent(Coverage.sum(files.map(Coverage.lines))),
|
|
2577
|
+
""
|
|
2578
|
+
),
|
|
2579
|
+
...files.map(
|
|
2580
|
+
(coverage) => row(
|
|
2581
|
+
` ${coverage.file}`,
|
|
2582
|
+
Coverage.percent(
|
|
2583
|
+
Coverage.functions(coverage),
|
|
2584
|
+
coverage.loaded
|
|
2585
|
+
),
|
|
2586
|
+
Coverage.percent(Coverage.lines(coverage), coverage.loaded),
|
|
2587
|
+
Coverage.uncovered(coverage)
|
|
2588
|
+
)
|
|
2589
|
+
)
|
|
2590
|
+
];
|
|
2591
|
+
const widths = header.map(
|
|
2592
|
+
(title, column) => Math.max(
|
|
2593
|
+
title.length,
|
|
2594
|
+
...rows.map((cells) => cells[column]?.length ?? 0)
|
|
2595
|
+
)
|
|
2596
|
+
);
|
|
2597
|
+
const line = /* @__PURE__ */ __name((cells, paint) => cells.map((cell, column) => {
|
|
2598
|
+
const width = widths[column] ?? 0;
|
|
2599
|
+
if (column === 1 || column === 2) {
|
|
2600
|
+
const padded = cell.padStart(width);
|
|
2601
|
+
if (!paint) return padded;
|
|
2602
|
+
const percent = Number(cell);
|
|
2603
|
+
return this.#paint(
|
|
2604
|
+
padded,
|
|
2605
|
+
percent >= 80 ? "green" : percent >= 50 ? "yellow" : "red"
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
return cell.padEnd(width);
|
|
2609
|
+
}).join(" | "), "line");
|
|
2610
|
+
const rule = widths.map((width) => "-".repeat(width)).join("-|-");
|
|
2611
|
+
this.#write(
|
|
2612
|
+
`${[
|
|
2613
|
+
"",
|
|
2614
|
+
rule,
|
|
2615
|
+
line(header, false),
|
|
2616
|
+
rule,
|
|
2617
|
+
...rows.map((cells) => line(cells, true)),
|
|
2618
|
+
rule
|
|
2619
|
+
].join("\n")}
|
|
2620
|
+
`
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
1988
2623
|
/** Explains that no test files were found. */
|
|
1989
2624
|
none(patterns) {
|
|
1990
2625
|
this.#write(
|
|
@@ -2053,7 +2688,7 @@ ${failed ? "\n" : ""}`);
|
|
|
2053
2688
|
const match = /^\s+at (?:.*?\()?(.+?):(\d+):(\d+)\)?$/.exec(line);
|
|
2054
2689
|
if (!match) continue;
|
|
2055
2690
|
let file = match[1];
|
|
2056
|
-
if (file.startsWith("file://")) file =
|
|
2691
|
+
if (file.startsWith("file://")) file = fileURLToPath2(file);
|
|
2057
2692
|
if (!isAbsolute(file) || file.includes("node_modules") || file.startsWith(_Reporter.#own) || file.startsWith(Transformer.prefix))
|
|
2058
2693
|
continue;
|
|
2059
2694
|
frames.push({
|
|
@@ -2067,7 +2702,7 @@ ${failed ? "\n" : ""}`);
|
|
|
2067
2702
|
/** The lines around a position, with a mark under it. */
|
|
2068
2703
|
#code({ file, line, column }) {
|
|
2069
2704
|
if (!existsSync2(file)) return "";
|
|
2070
|
-
const lines =
|
|
2705
|
+
const lines = readFileSync4(file, "utf8").split(/\r?\n/);
|
|
2071
2706
|
const first = Math.max(1, line - 2);
|
|
2072
2707
|
const last = Math.min(lines.length, line + 2);
|
|
2073
2708
|
const width = String(last).length;
|
|
@@ -2087,7 +2722,7 @@ ${failed ? "\n" : ""}`);
|
|
|
2087
2722
|
return out.join("\n");
|
|
2088
2723
|
}
|
|
2089
2724
|
#path(file) {
|
|
2090
|
-
const path =
|
|
2725
|
+
const path = relative2(this.#root, file);
|
|
2091
2726
|
return path.startsWith("..") ? file : path;
|
|
2092
2727
|
}
|
|
2093
2728
|
#paint(text, ...styles) {
|
|
@@ -2114,6 +2749,8 @@ var init_runner = __esm({
|
|
|
2114
2749
|
}
|
|
2115
2750
|
#settings;
|
|
2116
2751
|
#focused = false;
|
|
2752
|
+
/** Fails the test or hook that is running, if any. */
|
|
2753
|
+
#abort;
|
|
2117
2754
|
constructor(settings) {
|
|
2118
2755
|
this.#settings = settings;
|
|
2119
2756
|
}
|
|
@@ -2125,6 +2762,15 @@ var init_runner = __esm({
|
|
|
2125
2762
|
await this.#suite(root, [], results, errors);
|
|
2126
2763
|
return { results, errors };
|
|
2127
2764
|
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Fails the running test or hook with an error thrown outside of it, like
|
|
2767
|
+
* from a timer it started. Returns false when nothing is running.
|
|
2768
|
+
*/
|
|
2769
|
+
fail(error) {
|
|
2770
|
+
if (!this.#abort) return false;
|
|
2771
|
+
this.#abort(error);
|
|
2772
|
+
return true;
|
|
2773
|
+
}
|
|
2128
2774
|
/** Whether a test runs: not skipped, focused when others are, matching the name pattern. */
|
|
2129
2775
|
runs(test) {
|
|
2130
2776
|
return !test.skipped && (!this.#focused || test.focused) && (this.#settings.namePattern?.test(test.fullName) ?? true);
|
|
@@ -2202,6 +2848,7 @@ var init_runner = __esm({
|
|
|
2202
2848
|
async #call(body, timeout = this.#settings.timeout) {
|
|
2203
2849
|
let timer;
|
|
2204
2850
|
const expired = new Promise((_, reject) => {
|
|
2851
|
+
this.#abort = reject;
|
|
2205
2852
|
timer = setTimeout(
|
|
2206
2853
|
() => reject(
|
|
2207
2854
|
new Error(
|
|
@@ -2217,17 +2864,18 @@ var init_runner = __esm({
|
|
|
2217
2864
|
} catch (error) {
|
|
2218
2865
|
return error ?? new Error(`${String(error)} was thrown`);
|
|
2219
2866
|
} finally {
|
|
2867
|
+
this.#abort = void 0;
|
|
2220
2868
|
clearTimeout(timer);
|
|
2221
2869
|
}
|
|
2222
2870
|
}
|
|
2223
2871
|
/** Runs a body as a promise, with a `done` callback when it takes one. */
|
|
2224
2872
|
static #invoke(body) {
|
|
2225
2873
|
if (body.length === 0)
|
|
2226
|
-
return new Promise((
|
|
2874
|
+
return new Promise((resolve3) => resolve3(body(() => {
|
|
2227
2875
|
})));
|
|
2228
|
-
return new Promise((
|
|
2876
|
+
return new Promise((resolve3, reject) => {
|
|
2229
2877
|
body(
|
|
2230
|
-
(error) => error === void 0 ?
|
|
2878
|
+
(error) => error === void 0 ? resolve3(void 0) : reject(error)
|
|
2231
2879
|
);
|
|
2232
2880
|
});
|
|
2233
2881
|
}
|
|
@@ -2236,20 +2884,22 @@ var init_runner = __esm({
|
|
|
2236
2884
|
});
|
|
2237
2885
|
|
|
2238
2886
|
// packages/testero/src/testero.ts
|
|
2239
|
-
import {
|
|
2887
|
+
import { mkdirSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2888
|
+
import { isAbsolute as isAbsolute2, join as join4 } from "node:path";
|
|
2240
2889
|
var Testero;
|
|
2241
2890
|
var init_testero = __esm({
|
|
2242
2891
|
"packages/testero/src/testero.ts"() {
|
|
2243
2892
|
"use strict";
|
|
2244
2893
|
init_src();
|
|
2245
2894
|
init_collector();
|
|
2895
|
+
init_coverage();
|
|
2246
2896
|
init_expect();
|
|
2247
2897
|
init_globals();
|
|
2248
2898
|
init_mock();
|
|
2249
2899
|
init_reporter();
|
|
2250
2900
|
init_runner();
|
|
2251
2901
|
init_transformer();
|
|
2252
|
-
Testero = class {
|
|
2902
|
+
Testero = class _Testero {
|
|
2253
2903
|
static {
|
|
2254
2904
|
__name(this, "Testero");
|
|
2255
2905
|
}
|
|
@@ -2258,6 +2908,10 @@ var init_testero = __esm({
|
|
|
2258
2908
|
#reporter;
|
|
2259
2909
|
/** Errors thrown outside of tests, like from a forgotten timer. */
|
|
2260
2910
|
#stray = [];
|
|
2911
|
+
/** The runner of the file being run, which fails its running test with stray errors. */
|
|
2912
|
+
#runner;
|
|
2913
|
+
/** The test files and collectCoverageFrom, made when coverage first needs them. */
|
|
2914
|
+
#coverageIncludes;
|
|
2261
2915
|
constructor(config, options, reporter = new Reporter(config.root, { verbose: options.verbose })) {
|
|
2262
2916
|
this.#config = config;
|
|
2263
2917
|
this.#options = options;
|
|
@@ -2265,7 +2919,7 @@ var init_testero = __esm({
|
|
|
2265
2919
|
}
|
|
2266
2920
|
/** The test files to run, relative to the root. */
|
|
2267
2921
|
files() {
|
|
2268
|
-
const includes = new Includes(this.#options.testMatch);
|
|
2922
|
+
const includes = new Includes(this.#options.testMatch, "testMatch");
|
|
2269
2923
|
const patterns = this.#options.patterns.map(
|
|
2270
2924
|
(pattern) => new RegExp(pattern)
|
|
2271
2925
|
);
|
|
@@ -2281,11 +2935,18 @@ var init_testero = __esm({
|
|
|
2281
2935
|
this.#reporter.none(this.#options.patterns);
|
|
2282
2936
|
return this.#options.passWithNoTests;
|
|
2283
2937
|
}
|
|
2938
|
+
const coverage = this.#options.collectCoverage ? new Coverage(this.#config.root, (file) => this.#tracks(file)) : void 0;
|
|
2939
|
+
await coverage?.start();
|
|
2284
2940
|
process.setSourceMapsEnabled(true);
|
|
2285
|
-
const
|
|
2941
|
+
const runs = _Testero.#runs();
|
|
2942
|
+
const stray = /* @__PURE__ */ __name((error) => {
|
|
2943
|
+
if (runs.at(-1) !== stray) return;
|
|
2944
|
+
if (!this.#runner?.fail(error)) this.#stray.push(error);
|
|
2945
|
+
}, "stray");
|
|
2946
|
+
runs.push(stray);
|
|
2286
2947
|
process.on("uncaughtException", stray);
|
|
2287
2948
|
process.on("unhandledRejection", stray);
|
|
2288
|
-
const transformer = new Transformer();
|
|
2949
|
+
const transformer = new Transformer(coverage !== void 0);
|
|
2289
2950
|
const results = [];
|
|
2290
2951
|
try {
|
|
2291
2952
|
for (const file of files) {
|
|
@@ -2298,20 +2959,68 @@ var init_testero = __esm({
|
|
|
2298
2959
|
transformer.dispose();
|
|
2299
2960
|
process.off("uncaughtException", stray);
|
|
2300
2961
|
process.off("unhandledRejection", stray);
|
|
2962
|
+
runs.splice(runs.indexOf(stray), 1);
|
|
2963
|
+
}
|
|
2964
|
+
if (coverage) {
|
|
2965
|
+
const covered = await coverage.collect(
|
|
2966
|
+
transformer.bundles,
|
|
2967
|
+
this.#options.collectCoverageFrom.length > 0 ? [...this.#config.files()].filter(
|
|
2968
|
+
(file) => Transformer.loader(file) !== void 0 && this.#tracks(file)
|
|
2969
|
+
) : []
|
|
2970
|
+
);
|
|
2971
|
+
this.#reporter.coverage(covered);
|
|
2972
|
+
const directory = join4(
|
|
2973
|
+
this.#config.root,
|
|
2974
|
+
this.#options.coverageDirectory
|
|
2975
|
+
);
|
|
2976
|
+
mkdirSync(directory, { recursive: true });
|
|
2977
|
+
writeFileSync3(
|
|
2978
|
+
join4(directory, "lcov.info"),
|
|
2979
|
+
Coverage.lcov(this.#config.root, covered)
|
|
2980
|
+
);
|
|
2301
2981
|
}
|
|
2302
2982
|
this.#reporter.summary(results, performance.now() - start);
|
|
2303
2983
|
return !results.some(Reporter.failed);
|
|
2304
2984
|
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Whether coverage is reported for a file, relative to the root: one of
|
|
2987
|
+
* the project, not a test file nor installed, and in collectCoverageFrom
|
|
2988
|
+
* when it is set.
|
|
2989
|
+
*/
|
|
2990
|
+
#tracks(file) {
|
|
2991
|
+
if (file.startsWith("../") || isAbsolute2(file)) return false;
|
|
2992
|
+
if (this.#config.ignore.matches(file)) return false;
|
|
2993
|
+
if (file.split("/").includes("node_modules")) return false;
|
|
2994
|
+
this.#coverageIncludes ??= {
|
|
2995
|
+
tests: new Includes(this.#options.testMatch, "testMatch"),
|
|
2996
|
+
from: this.#options.collectCoverageFrom.length > 0 ? new Includes(
|
|
2997
|
+
this.#options.collectCoverageFrom,
|
|
2998
|
+
"collectCoverageFrom"
|
|
2999
|
+
) : void 0
|
|
3000
|
+
};
|
|
3001
|
+
const { tests, from } = this.#coverageIncludes;
|
|
3002
|
+
return !tests.matches(file) && (from?.matches(file) ?? true);
|
|
3003
|
+
}
|
|
3004
|
+
/**
|
|
3005
|
+
* The stray error listeners of the runs in progress, innermost last.
|
|
3006
|
+
* Kept on globalThis, as nested runs may come from different copies of this module.
|
|
3007
|
+
*/
|
|
3008
|
+
static #runs() {
|
|
3009
|
+
const key = /* @__PURE__ */ Symbol.for("@aljazmc/testero.runs");
|
|
3010
|
+
const global = globalThis;
|
|
3011
|
+
global[key] ??= [];
|
|
3012
|
+
return global[key];
|
|
3013
|
+
}
|
|
2305
3014
|
async #file(file, transformer) {
|
|
2306
3015
|
const start = performance.now();
|
|
2307
|
-
const finish = /* @__PURE__ */ __name((
|
|
2308
|
-
|
|
3016
|
+
const finish = /* @__PURE__ */ __name((results, errors) => {
|
|
3017
|
+
errors.push(...this.#stray.splice(0));
|
|
2309
3018
|
Mocks.restoreAll();
|
|
2310
3019
|
Mocks.forget();
|
|
2311
3020
|
return {
|
|
2312
3021
|
file,
|
|
2313
|
-
results
|
|
2314
|
-
errors
|
|
3022
|
+
results,
|
|
3023
|
+
errors,
|
|
2315
3024
|
duration: performance.now() - start
|
|
2316
3025
|
};
|
|
2317
3026
|
}, "finish");
|
|
@@ -2330,7 +3039,7 @@ var init_testero = __esm({
|
|
|
2330
3039
|
};
|
|
2331
3040
|
Globals.install(collector, expect, settings);
|
|
2332
3041
|
try {
|
|
2333
|
-
await import(await transformer.compile(
|
|
3042
|
+
await import(await transformer.compile(join4(this.#config.root, file)));
|
|
2334
3043
|
} catch (error) {
|
|
2335
3044
|
return finish([], [error]);
|
|
2336
3045
|
} finally {
|
|
@@ -2341,10 +3050,13 @@ var init_testero = __esm({
|
|
|
2341
3050
|
[],
|
|
2342
3051
|
[new Error("Your test suite must contain at least one test.")]
|
|
2343
3052
|
);
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
3053
|
+
this.#runner = new Runner(settings);
|
|
3054
|
+
try {
|
|
3055
|
+
const { results, errors } = await this.#runner.run(collector.root);
|
|
3056
|
+
return finish(results, errors);
|
|
3057
|
+
} finally {
|
|
3058
|
+
this.#runner = void 0;
|
|
3059
|
+
}
|
|
2348
3060
|
}
|
|
2349
3061
|
};
|
|
2350
3062
|
}
|
|
@@ -2370,6 +3082,9 @@ Options:
|
|
|
2370
3082
|
--listTests print the test files instead of running them
|
|
2371
3083
|
--passWithNoTests succeed when there are no test files
|
|
2372
3084
|
--testTimeout <ms> milliseconds a test or hook may take
|
|
3085
|
+
--coverage report which lines and functions the tests run
|
|
3086
|
+
--watch rerun the tests of changed files and what imports them
|
|
3087
|
+
--watchAll rerun every test when a file changes
|
|
2373
3088
|
--config [js] write the default testero.config.ts, or .js
|
|
2374
3089
|
-h, --help show this help
|
|
2375
3090
|
`;
|
|
@@ -2378,9 +3093,14 @@ Options:
|
|
|
2378
3093
|
namePattern;
|
|
2379
3094
|
listTests = false;
|
|
2380
3095
|
help = false;
|
|
3096
|
+
/** Reruns the tests of changed files, or all of them, when files change. */
|
|
3097
|
+
watch;
|
|
3098
|
+
/** The options, without the patterns and the watch ones, for the runs of watch mode. */
|
|
3099
|
+
flags = [];
|
|
2381
3100
|
constructor(argv) {
|
|
2382
3101
|
for (let index = 0; index < argv.length; index++) {
|
|
2383
3102
|
const argument = argv[index];
|
|
3103
|
+
const start = index;
|
|
2384
3104
|
const [name, inline] = argument.split(/=(.*)/s);
|
|
2385
3105
|
const value = /* @__PURE__ */ __name(() => {
|
|
2386
3106
|
const next = inline ?? argv[++index];
|
|
@@ -2393,14 +3113,32 @@ Options:
|
|
|
2393
3113
|
case "--testNamePattern":
|
|
2394
3114
|
this.namePattern = new RegExp(value());
|
|
2395
3115
|
break;
|
|
2396
|
-
case "--testTimeout":
|
|
2397
|
-
|
|
3116
|
+
case "--testTimeout": {
|
|
3117
|
+
const text = value();
|
|
3118
|
+
const timeout = Number(text);
|
|
3119
|
+
const valid = timeout > 0 && timeout < 2 ** 31;
|
|
3120
|
+
if (text.trim() === "" || !valid)
|
|
3121
|
+
throw new Error(
|
|
3122
|
+
`--testTimeout needs a positive number of milliseconds, not "${text}".`
|
|
3123
|
+
);
|
|
3124
|
+
this.rules.testTimeout = timeout;
|
|
2398
3125
|
break;
|
|
3126
|
+
}
|
|
2399
3127
|
case "--bail":
|
|
2400
3128
|
case "--verbose":
|
|
2401
3129
|
case "--passWithNoTests":
|
|
2402
3130
|
this.rules[name.slice(2)] = true;
|
|
2403
3131
|
break;
|
|
3132
|
+
case "--coverage":
|
|
3133
|
+
case "--collectCoverage":
|
|
3134
|
+
this.rules.collectCoverage = true;
|
|
3135
|
+
break;
|
|
3136
|
+
case "--watch":
|
|
3137
|
+
this.watch ??= "related";
|
|
3138
|
+
continue;
|
|
3139
|
+
case "--watchAll":
|
|
3140
|
+
this.watch = "all";
|
|
3141
|
+
continue;
|
|
2404
3142
|
case "--listTests":
|
|
2405
3143
|
this.listTests = true;
|
|
2406
3144
|
break;
|
|
@@ -2412,7 +3150,9 @@ Options:
|
|
|
2412
3150
|
if (name.startsWith("-"))
|
|
2413
3151
|
throw new Error(`Unknown option ${name}.`);
|
|
2414
3152
|
this.patterns.push(argument);
|
|
3153
|
+
continue;
|
|
2415
3154
|
}
|
|
3155
|
+
this.flags.push(...argv.slice(start, index + 1));
|
|
2416
3156
|
}
|
|
2417
3157
|
}
|
|
2418
3158
|
/** The configured rules, with the ones given on the command line instead. */
|
|
@@ -2445,7 +3185,10 @@ var init_config2 = __esm({
|
|
|
2445
3185
|
passWithNoTests: false,
|
|
2446
3186
|
clearMocks: false,
|
|
2447
3187
|
resetMocks: false,
|
|
2448
|
-
restoreMocks: false
|
|
3188
|
+
restoreMocks: false,
|
|
3189
|
+
collectCoverage: false,
|
|
3190
|
+
collectCoverageFrom: [],
|
|
3191
|
+
coverageDirectory: "coverage"
|
|
2449
3192
|
};
|
|
2450
3193
|
config_default = {
|
|
2451
3194
|
ignore: [".git", "node_modules"],
|
|
@@ -2454,8 +3197,166 @@ var init_config2 = __esm({
|
|
|
2454
3197
|
}
|
|
2455
3198
|
});
|
|
2456
3199
|
|
|
3200
|
+
// packages/testero/src/watcher.ts
|
|
3201
|
+
var Watcher, Dependencies;
|
|
3202
|
+
var init_watcher = __esm({
|
|
3203
|
+
"packages/testero/src/watcher.ts"() {
|
|
3204
|
+
"use strict";
|
|
3205
|
+
Watcher = class _Watcher {
|
|
3206
|
+
static {
|
|
3207
|
+
__name(this, "Watcher");
|
|
3208
|
+
}
|
|
3209
|
+
#options;
|
|
3210
|
+
#write;
|
|
3211
|
+
#current;
|
|
3212
|
+
#queued;
|
|
3213
|
+
#last = "all";
|
|
3214
|
+
#changed = /* @__PURE__ */ new Set();
|
|
3215
|
+
#timer;
|
|
3216
|
+
#unsubscribe;
|
|
3217
|
+
#closed = false;
|
|
3218
|
+
#keys = /* @__PURE__ */ __name((data) => this.#key(String(data)), "#keys");
|
|
3219
|
+
constructor(options) {
|
|
3220
|
+
this.#options = options;
|
|
3221
|
+
this.#write = options.write ?? ((text) => process.stdout.write(text));
|
|
3222
|
+
}
|
|
3223
|
+
/** Runs all tests, then watches. */
|
|
3224
|
+
start() {
|
|
3225
|
+
this.#unsubscribe = this.#options.subscribe(
|
|
3226
|
+
(path) => this.#change(path)
|
|
3227
|
+
);
|
|
3228
|
+
const input = this.#options.input;
|
|
3229
|
+
if (input) {
|
|
3230
|
+
input.setRawMode?.(true);
|
|
3231
|
+
input.on("data", this.#keys);
|
|
3232
|
+
input.resume();
|
|
3233
|
+
}
|
|
3234
|
+
this.#queue("all");
|
|
3235
|
+
}
|
|
3236
|
+
/** Stops watching and the run in progress. */
|
|
3237
|
+
close() {
|
|
3238
|
+
this.#closed = true;
|
|
3239
|
+
clearTimeout(this.#timer);
|
|
3240
|
+
this.#unsubscribe?.();
|
|
3241
|
+
this.#current?.run.stop();
|
|
3242
|
+
const input = this.#options.input;
|
|
3243
|
+
if (input) {
|
|
3244
|
+
input.off("data", this.#keys);
|
|
3245
|
+
input.setRawMode?.(false);
|
|
3246
|
+
input.pause();
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
#key(key) {
|
|
3250
|
+
if (key === "q" || key === "" || key === "") this.close();
|
|
3251
|
+
else if (key === "a") this.#queue("all");
|
|
3252
|
+
else if (key === "\r" || key === "\n") this.#queue(this.#last);
|
|
3253
|
+
}
|
|
3254
|
+
#change(path) {
|
|
3255
|
+
if (this.#closed || this.#options.ignores?.(path)) return;
|
|
3256
|
+
this.#changed.add(path);
|
|
3257
|
+
clearTimeout(this.#timer);
|
|
3258
|
+
this.#timer = setTimeout(
|
|
3259
|
+
() => void this.#flush(),
|
|
3260
|
+
this.#options.delay ?? 100
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
async #flush() {
|
|
3264
|
+
const changed = this.#changed;
|
|
3265
|
+
this.#changed = /* @__PURE__ */ new Set();
|
|
3266
|
+
if (this.#options.mode === "all") return this.#queue("all");
|
|
3267
|
+
try {
|
|
3268
|
+
const files = await this.#options.related(changed);
|
|
3269
|
+
if (files.length > 0) this.#queue(files);
|
|
3270
|
+
} catch (error) {
|
|
3271
|
+
this.#write(
|
|
3272
|
+
`${error instanceof Error ? error.message : String(error)}
|
|
3273
|
+
`
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
/** Runs a selection, after stopping the run in progress, whose files run again too. */
|
|
3278
|
+
#queue(selection) {
|
|
3279
|
+
if (this.#closed) return;
|
|
3280
|
+
this.#queued = _Watcher.merge(this.#queued, selection);
|
|
3281
|
+
if (this.#current) {
|
|
3282
|
+
this.#queued = _Watcher.merge(this.#queued, this.#current.selection);
|
|
3283
|
+
this.#current.run.stop();
|
|
3284
|
+
} else this.#next();
|
|
3285
|
+
}
|
|
3286
|
+
#next() {
|
|
3287
|
+
const selection = this.#queued;
|
|
3288
|
+
if (selection === void 0 || this.#closed) return;
|
|
3289
|
+
this.#queued = void 0;
|
|
3290
|
+
this.#last = selection;
|
|
3291
|
+
if (this.#options.clear) this.#write("\x1B[2J\x1B[3J\x1B[H");
|
|
3292
|
+
const run = this.#options.run(
|
|
3293
|
+
selection === "all" ? void 0 : selection
|
|
3294
|
+
);
|
|
3295
|
+
this.#current = { selection, run };
|
|
3296
|
+
void run.done.then(() => {
|
|
3297
|
+
this.#current = void 0;
|
|
3298
|
+
if (this.#queued !== void 0) this.#next();
|
|
3299
|
+
else if (!this.#closed) this.#usage();
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
#usage() {
|
|
3303
|
+
this.#write(
|
|
3304
|
+
this.#options.input ? [
|
|
3305
|
+
"",
|
|
3306
|
+
"Watch Usage",
|
|
3307
|
+
" \u203A Press a to run all tests.",
|
|
3308
|
+
" \u203A Press Enter to run the last tests again.",
|
|
3309
|
+
" \u203A Press q to quit watch mode.",
|
|
3310
|
+
""
|
|
3311
|
+
].join("\n") : "\nWatching for changes, Ctrl+C quits.\n"
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
/** Two selections in one, all when either is. */
|
|
3315
|
+
static merge(a, b) {
|
|
3316
|
+
if (a === void 0) return b;
|
|
3317
|
+
if (a === "all" || b === "all") return "all";
|
|
3318
|
+
return [.../* @__PURE__ */ new Set([...a, ...b])].sort();
|
|
3319
|
+
}
|
|
3320
|
+
};
|
|
3321
|
+
Dependencies = class {
|
|
3322
|
+
static {
|
|
3323
|
+
__name(this, "Dependencies");
|
|
3324
|
+
}
|
|
3325
|
+
#list;
|
|
3326
|
+
#inputs = /* @__PURE__ */ new Map();
|
|
3327
|
+
/** `list` gives the files a test file is bundled from, all relative to the root. */
|
|
3328
|
+
constructor(list) {
|
|
3329
|
+
this.#list = list;
|
|
3330
|
+
}
|
|
3331
|
+
async related(tests, changed) {
|
|
3332
|
+
const related = [];
|
|
3333
|
+
for (const test of tests) {
|
|
3334
|
+
const known = this.#inputs.get(test);
|
|
3335
|
+
const touched = /* @__PURE__ */ __name((inputs2) => inputs2 !== void 0 && [...inputs2].some((input) => changed.has(input)), "touched");
|
|
3336
|
+
let inputs = known;
|
|
3337
|
+
if (!known || changed.has(test) || touched(known))
|
|
3338
|
+
try {
|
|
3339
|
+
inputs = new Set(await this.#list(test));
|
|
3340
|
+
this.#inputs.set(test, inputs);
|
|
3341
|
+
} catch {
|
|
3342
|
+
this.#inputs.delete(test);
|
|
3343
|
+
related.push(test);
|
|
3344
|
+
continue;
|
|
3345
|
+
}
|
|
3346
|
+
if (changed.has(test) || touched(known) || touched(inputs))
|
|
3347
|
+
related.push(test);
|
|
3348
|
+
}
|
|
3349
|
+
return related;
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
});
|
|
3354
|
+
|
|
2457
3355
|
// packages/testero/src/index.ts
|
|
2458
3356
|
var index_exports = {};
|
|
3357
|
+
import { spawn } from "node:child_process";
|
|
3358
|
+
import { watch } from "node:fs";
|
|
3359
|
+
import { join as join5, relative as relative3, sep as sep2 } from "node:path";
|
|
2459
3360
|
var init_index = __esm({
|
|
2460
3361
|
async "packages/testero/src/index.ts"() {
|
|
2461
3362
|
"use strict";
|
|
@@ -2463,16 +3364,70 @@ var init_index = __esm({
|
|
|
2463
3364
|
init_arguments();
|
|
2464
3365
|
init_config2();
|
|
2465
3366
|
init_testero();
|
|
3367
|
+
init_transformer();
|
|
3368
|
+
init_watcher();
|
|
2466
3369
|
try {
|
|
2467
3370
|
if (Config.init("testero", config_default)) process.exit();
|
|
2468
3371
|
const args = new Arguments(process.argv.slice(2));
|
|
2469
3372
|
if (args.help) process.stdout.write(Arguments.usage);
|
|
2470
3373
|
else {
|
|
2471
3374
|
const config = await Config.load("testero", config_default);
|
|
2472
|
-
const
|
|
3375
|
+
const options = args.options(config.rules);
|
|
3376
|
+
const testero = new Testero(config, options);
|
|
2473
3377
|
if (args.listTests)
|
|
2474
3378
|
for (const file of testero.files()) console.log(file);
|
|
2475
|
-
else if (
|
|
3379
|
+
else if (args.watch) {
|
|
3380
|
+
const entry = process.argv[1];
|
|
3381
|
+
const exactly = /* @__PURE__ */ __name((file) => `^${file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "exactly");
|
|
3382
|
+
const transformer = new Transformer();
|
|
3383
|
+
const dependencies = new Dependencies(
|
|
3384
|
+
async (file) => (await transformer.inputs(join5(config.root, file))).map(
|
|
3385
|
+
(input) => relative3(config.root, input).split(sep2).join("/")
|
|
3386
|
+
)
|
|
3387
|
+
);
|
|
3388
|
+
const coverage = new Pattern(`/${options.coverageDirectory}`);
|
|
3389
|
+
const watcher = new Watcher({
|
|
3390
|
+
mode: args.watch,
|
|
3391
|
+
run: /* @__PURE__ */ __name((files) => {
|
|
3392
|
+
const child = spawn(
|
|
3393
|
+
process.execPath,
|
|
3394
|
+
[
|
|
3395
|
+
...process.execArgv,
|
|
3396
|
+
entry,
|
|
3397
|
+
...args.flags,
|
|
3398
|
+
...files?.map(exactly) ?? args.patterns
|
|
3399
|
+
],
|
|
3400
|
+
{ stdio: ["ignore", "inherit", "inherit"] }
|
|
3401
|
+
);
|
|
3402
|
+
return {
|
|
3403
|
+
done: new Promise(
|
|
3404
|
+
(resolve3) => child.on("close", resolve3).on("error", resolve3)
|
|
3405
|
+
),
|
|
3406
|
+
stop: /* @__PURE__ */ __name(() => child.kill(), "stop")
|
|
3407
|
+
};
|
|
3408
|
+
}, "run"),
|
|
3409
|
+
related: /* @__PURE__ */ __name((changed) => dependencies.related(testero.files(), changed), "related"),
|
|
3410
|
+
subscribe: /* @__PURE__ */ __name((listener) => {
|
|
3411
|
+
const watching = watch(
|
|
3412
|
+
config.root,
|
|
3413
|
+
{ recursive: true },
|
|
3414
|
+
(_, name) => {
|
|
3415
|
+
if (name) listener(name.split(sep2).join("/"));
|
|
3416
|
+
}
|
|
3417
|
+
);
|
|
3418
|
+
return () => watching.close();
|
|
3419
|
+
}, "subscribe"),
|
|
3420
|
+
// sources and JSON, like Jest's moduleFileExtensions, not
|
|
3421
|
+
// the coverage a run writes
|
|
3422
|
+
ignores: /* @__PURE__ */ __name((path) => config.ignore.matches(path) || coverage.matches(path) || Transformer.loader(path) === void 0 && !path.endsWith(".json"), "ignores"),
|
|
3423
|
+
input: process.stdin.isTTY ? process.stdin : void 0,
|
|
3424
|
+
clear: process.stdout.isTTY
|
|
3425
|
+
});
|
|
3426
|
+
process.on("exit", () => transformer.dispose());
|
|
3427
|
+
for (const signal of ["SIGINT", "SIGTERM"])
|
|
3428
|
+
process.once(signal, () => watcher.close());
|
|
3429
|
+
watcher.start();
|
|
3430
|
+
} else if (!await testero.run()) process.exitCode = 1;
|
|
2476
3431
|
}
|
|
2477
3432
|
} catch (error) {
|
|
2478
3433
|
console.error(error instanceof Error ? error.message : error);
|