@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/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
- constructor(source) {
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(`^${_Pattern.#toRegex(glob)}$`);
30
+ this.#anchored = glob.includes("/") || /^\.?\//.test(source);
31
+ this.#regex = new RegExp(
32
+ `^${_Pattern.#toRegex(glob, source, setting)}$`
33
+ );
29
34
  }
30
- matches(path) {
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
- (segment, index) => this.#regex.test(
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 in tokens) regex += tokens[token] ?? 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
- matches(path) {
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((pattern) => pattern.matches(normalized));
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 { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
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.indexOf("--config");
170
+ const index = argv.findIndex(
171
+ (arg) => arg === "--config" || arg.startsWith("--config=")
172
+ );
129
173
  if (index === -1) return false;
130
- const next = argv[index + 1];
131
- const extension = next === void 0 || next.startsWith("-") ? "ts" : next;
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
- const entries = readdirSync(join(this.root, directory), {
193
- withFileTypes: true
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
- if (this.ignore.matches(path)) continue;
198
- if (entry.isDirectory()) yield* this.files(path);
199
- else yield path;
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
- /** Text files that are not ignored and that accepts takes, binaries are skipped. */
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
- const text = readFileSync(path, "utf8");
208
- if (!text.includes("\0")) yield { file, path, text };
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
- constructor(globs) {
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.join("").trim().split(/\s*\|\s*/);
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
- () => body(...row),
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/expect/equality.ts
792
- var Equality;
793
- var init_equality = __esm({
794
- "packages/testero/src/expect/equality.ts"() {
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
- Equality = class _Equality {
1072
+ init_mock();
1073
+ Globals = class _Globals {
797
1074
  static {
798
- __name(this, "Equality");
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
- /** Whether received has every property expected has, recursively, like `toMatchObject`. */
812
- static matches(received, expected) {
813
- if (_Equality.asymmetric(expected))
814
- return expected.asymmetricMatch(received);
815
- if (!_Equality.#plainish(expected) || typeof received !== "object" || received === null)
816
- return _Equality.equals(received, expected);
817
- if (Array.isArray(expected))
818
- return Array.isArray(received) && received.length === expected.length && expected.every(
819
- (item, index) => _Equality.matches(received[index], item)
820
- );
821
- return _Equality.#keys(expected, true).every(
822
- (key) => key in received && _Equality.matches(
823
- received[key],
824
- expected[key]
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
- static #equals(a, b, strict, aSeen, bSeen) {
833
- if (_Equality.asymmetric(a)) return a.asymmetricMatch(b);
834
- if (_Equality.asymmetric(b)) return b.asymmetricMatch(a);
835
- if (Object.is(a, b)) return true;
836
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
837
- return false;
838
- const tag = Object.prototype.toString.call(a);
839
- if (tag !== Object.prototype.toString.call(b)) return false;
840
- if (strict && Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
841
- return false;
842
- if (a instanceof Date)
843
- return Object.is(a.getTime(), b.getTime());
844
- if (a instanceof RegExp) return String(a) === String(b);
845
- if (a instanceof Error && b instanceof Error)
846
- return a.name === b.name && a.message === b.message;
847
- if (a instanceof Number || a instanceof String || a instanceof Boolean)
848
- return Object.is(
849
- a.valueOf(),
850
- b.valueOf()
851
- );
852
- const index = aSeen.indexOf(a);
853
- if (index >= 0) return bSeen[index] === b;
854
- aSeen.push(a);
855
- bSeen.push(b);
856
- try {
857
- return _Equality.#structures(a, b, strict, aSeen, bSeen);
858
- } finally {
859
- aSeen.pop();
860
- bSeen.pop();
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
- static #structures(a, b, strict, aSeen, bSeen) {
864
- const equal = /* @__PURE__ */ __name((x, y) => _Equality.#equals(x, y, strict, aSeen, bSeen), "equal");
865
- if (a instanceof Map && b instanceof Map)
866
- return a.size === b.size && [...a].every(
867
- ([key, value]) => b.has(key) ? equal(value, b.get(key)) : [...b].some(
868
- ([other, item]) => equal(key, other) && equal(value, item)
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
- if (a instanceof Set && b instanceof Set)
872
- return a.size === b.size && [...a].every(
873
- (value) => b.has(value) || [...b].some((other) => equal(value, other))
874
- );
875
- if (Array.isArray(a) || ArrayBuffer.isView(a)) {
876
- const left = a;
877
- const right = b;
878
- if (left.length !== right.length) return false;
879
- for (let index = 0; index < left.length; index++) {
880
- if (strict && index in left !== index in right) return false;
881
- if (!equal(left[index], right[index])) return false;
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
- const indexed = Array.isArray(a) || ArrayBuffer.isView(a);
885
- const keysOf = /* @__PURE__ */ __name((object) => _Equality.#keys(object, strict).filter(
886
- (key) => !(indexed && typeof key === "string" && /^\d+$/.test(key))
887
- ), "keysOf");
888
- const aKeys = keysOf(a);
889
- const bKeys = keysOf(b);
890
- if (aKeys.length !== bKeys.length) return false;
891
- return aKeys.every(
892
- (key) => Object.hasOwn(b, key) && equal(
893
- a[key],
894
- b[key]
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
- /** Own enumerable keys, without the ones set to undefined unless strict. */
899
- static #keys(object, strict) {
900
- const record = object;
901
- const keys = [
902
- ...Object.keys(object),
903
- ...Object.getOwnPropertySymbols(object).filter(
904
- (symbol) => Object.getOwnPropertyDescriptor(object, symbol)?.enumerable
905
- )
906
- ];
907
- return strict ? keys : keys.filter((key) => record[key] !== void 0);
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 new _Assertion(this.#received, {
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 new _Assertion(this.#received, {
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 new _Assertion(this.#received, {
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
- promise: "rejects"
2163
+ ...change
1395
2164
  });
1396
2165
  }
1397
- /** Adds a matcher to every assertion, like `expect.extend`. */
1398
- static define(name, matcher) {
1399
- matchers[name] = matcher;
1400
- Object.defineProperty(_Assertion.prototype, name, {
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(name, this.#received, args, origin);
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(name, value, args, origin);
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
- #check(name, received, args, origin) {
1439
- const matcher = matchers[name];
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
- error instanceof Error ? error.message : String(error),
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
- init_globals();
1761
- Transformer = class _Transformer {
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, "Transformer");
2399
+ __name(this, "Expect");
1764
2400
  }
1765
- /** Packages test files import the globals from, like with Jest. */
1766
- static modules = ["@jest/globals", "@aljazmc/testero"];
1767
- static #loaders = {
1768
- ".js": "js",
1769
- ".cjs": "js",
1770
- ".mjs": "js",
1771
- ".jsx": "jsx",
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
- /** The import.meta properties that describe the source file. */
1778
- static #meta = ["url", "filename", "dirname"];
1779
- /** Marks the resolutions the plugin asks esbuild for itself. */
1780
- static #own = /* @__PURE__ */ Symbol("testero");
1781
- /** Where compiled test files go, stack frames there are testero's own. */
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
- #plugin() {
1824
- return {
1825
- name: "testero",
1826
- setup: /* @__PURE__ */ __name((build2) => {
1827
- build2.onResolve({ filter: /^testero-meta:/ }, (args) => ({
1828
- path: args.path.slice("testero-meta:".length),
1829
- namespace: "testero-meta"
1830
- }));
1831
- build2.onLoad(
1832
- { filter: /.*/, namespace: "testero-meta" },
1833
- (args) => ({
1834
- contents: `export default ${JSON.stringify({
1835
- url: pathToFileURL2(args.path).href,
1836
- filename: args.path,
1837
- dirname: dirname(args.path)
1838
- })};`,
1839
- loader: "js"
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
- static #installed(path) {
1878
- return path.split(sep).includes("node_modules");
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 readFileSync3 } from "node:fs";
1886
- import { dirname as dirname2, isAbsolute, relative } from "node:path";
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 = dirname2(fileURLToPath(import.meta.url));
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 = fileURLToPath(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 = readFileSync3(file, "utf8").split(/\r?\n/);
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 = relative(this.#root, file);
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((resolve) => resolve(body(() => {
2874
+ return new Promise((resolve3) => resolve3(body(() => {
2227
2875
  })));
2228
- return new Promise((resolve, reject) => {
2876
+ return new Promise((resolve3, reject) => {
2229
2877
  body(
2230
- (error) => error === void 0 ? resolve(void 0) : reject(error)
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 { join as join3 } from "node:path";
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 stray = /* @__PURE__ */ __name((error) => this.#stray.push(error), "stray");
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((results2, errors2) => {
2308
- errors2.push(...this.#stray.splice(0));
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: results2,
2314
- errors: errors2,
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(join3(this.#config.root, file)));
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
- const { results, errors } = await new Runner(settings).run(
2345
- collector.root
2346
- );
2347
- return finish(results, errors);
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
- this.rules.testTimeout = Number(value());
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 testero = new Testero(config, args.options(config.rules));
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 (!await testero.run()) process.exitCode = 1;
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);