@aljazmc/testero 0.4.3 → 0.5.1

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