@aljazmc/testero 0.1.6 → 0.2.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
@@ -1,12 +1,2140 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // shared/config/src/config.ts
4
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ // shared/config/src/pattern.ts
9
+ var Pattern = class _Pattern {
10
+ #anchored;
11
+ #regex;
12
+ constructor(source) {
13
+ const glob = source.replace(/^\.?\/+|\/+$/g, "");
14
+ this.#anchored = glob.includes("/");
15
+ this.#regex = new RegExp(`^${_Pattern.#toRegex(glob)}$`);
16
+ }
17
+ matches(path) {
18
+ const segments = path.split("/");
19
+ return segments.some(
20
+ (segment, index) => this.#regex.test(
21
+ this.#anchored ? segments.slice(0, index + 1).join("/") : segment
22
+ )
23
+ );
24
+ }
25
+ /** Translates `**`, `*`, `?` and `{a,b}` alternatives, other characters match themselves. */
26
+ static #toRegex(glob) {
27
+ const tokens = {
28
+ "**/": "(?:.*/)?",
29
+ "**": ".*",
30
+ "*": "[^/]*",
31
+ "?": "[^/]"
32
+ };
33
+ let regex = "";
34
+ let braces = 0;
35
+ for (const [token] of glob.matchAll(/\*\*\/|\*\*|[\s\S]/g)) {
36
+ if (token in tokens) regex += tokens[token] ?? token;
37
+ else if (token === "{") {
38
+ braces++;
39
+ regex += "(?:";
40
+ } else if (token === "}" && braces > 0) {
41
+ braces--;
42
+ regex += ")";
43
+ } else if (token === "," && braces > 0) regex += "|";
44
+ else regex += token.replace(/[.+^${}()|[\]\\]/g, "\\$&");
45
+ }
46
+ return regex;
47
+ }
48
+ };
49
+
50
+ // shared/config/src/ignore.ts
51
+ var Ignore = class {
52
+ #patterns;
53
+ constructor(sources) {
54
+ this.#patterns = sources.map((source) => new Pattern(source));
55
+ }
56
+ matches(path) {
57
+ const normalized = path.replaceAll("\\", "/").replace(/^\.\//, "");
58
+ return this.#patterns.some((pattern) => pattern.matches(normalized));
59
+ }
60
+ };
61
+
62
+ // shared/config/src/config.ts
63
+ var Config = class _Config {
64
+ static extensions = ["ts", "js"];
65
+ root;
66
+ ignore;
67
+ rules;
68
+ /** What the config file set, before the defaults were merged in. */
69
+ custom;
70
+ constructor(options, root = process.cwd(), custom = {}) {
71
+ this.root = root;
72
+ this.ignore = new Ignore(options.ignore);
73
+ this.rules = options.rules;
74
+ this.custom = custom;
75
+ }
76
+ static async load(name, defaults, root = process.cwd()) {
77
+ for (const extension of _Config.extensions) {
78
+ const file = join(root, `${name}.config.${extension}`);
79
+ if (existsSync(file)) {
80
+ const { default: options } = await import(pathToFileURL(file).href);
81
+ return new _Config(
82
+ _Config.merge(defaults, options),
83
+ root,
84
+ options
85
+ );
86
+ }
87
+ }
88
+ return new _Config(defaults, root);
89
+ }
90
+ /** Deep merges plain objects, anything else in override replaces base. */
91
+ static merge(base, override) {
92
+ const plain = (value) => value?.constructor === Object;
93
+ if (!plain(base) || !plain(override)) return override ?? base;
94
+ const merged = { ...base };
95
+ for (const [key, value] of Object.entries(override))
96
+ merged[key] = _Config.merge(base[key], value);
97
+ return merged;
98
+ }
99
+ *files(directory = "") {
100
+ const entries = readdirSync(join(this.root, directory), {
101
+ withFileTypes: true
102
+ });
103
+ for (const entry of entries) {
104
+ const path = directory ? `${directory}/${entry.name}` : entry.name;
105
+ if (this.ignore.matches(path)) continue;
106
+ if (entry.isDirectory()) yield* this.files(path);
107
+ else yield path;
108
+ }
109
+ }
110
+ /** Text files that are not ignored and that accepts takes, binaries are skipped. */
111
+ *sources(accepts = () => true) {
112
+ for (const file of this.files()) {
113
+ if (!accepts(file)) continue;
114
+ const path = join(this.root, file);
115
+ const text = readFileSync(path, "utf8");
116
+ if (!text.includes("\0")) yield { file, path, text };
117
+ }
118
+ }
119
+ };
120
+
121
+ // shared/config/src/includes.ts
122
+ var Includes = class {
123
+ #globs;
124
+ constructor(globs) {
125
+ this.#globs = globs.map((glob) => ({
126
+ pattern: new Pattern(glob.replace(/^!/, "")),
127
+ included: !glob.startsWith("!")
128
+ }));
129
+ }
130
+ matches(file) {
131
+ let included = false;
132
+ for (const { pattern, included: positive } of this.#globs)
133
+ if (pattern.matches(file)) included = positive;
134
+ return included;
135
+ }
136
+ };
137
+
138
+ // packages/testero/src/arguments.ts
139
+ var Arguments = class {
140
+ static usage = `Usage: testero [patterns...] [options]
141
+
142
+ Runs the test files whose paths match a pattern, every test file without patterns.
143
+
144
+ Options:
145
+ -t, --testNamePattern <regex> run only tests whose name matches
146
+ --bail stop after the first failing test file
147
+ --verbose list every test
148
+ --listTests print the test files instead of running them
149
+ --passWithNoTests succeed when there are no test files
150
+ --testTimeout <ms> milliseconds a test or hook may take
151
+ -h, --help show this help
152
+ `;
153
+ patterns = [];
154
+ rules = {};
155
+ namePattern;
156
+ listTests = false;
157
+ help = false;
158
+ constructor(argv) {
159
+ for (let index = 0; index < argv.length; index++) {
160
+ const argument = argv[index];
161
+ const [name, inline] = argument.split(/=(.*)/s);
162
+ const value = () => {
163
+ const next = inline ?? argv[++index];
164
+ if (next === void 0)
165
+ throw new Error(`${name} needs a value.`);
166
+ return next;
167
+ };
168
+ switch (name) {
169
+ case "-t":
170
+ case "--testNamePattern":
171
+ this.namePattern = new RegExp(value());
172
+ break;
173
+ case "--testTimeout":
174
+ this.rules.testTimeout = Number(value());
175
+ break;
176
+ case "--bail":
177
+ case "--verbose":
178
+ case "--passWithNoTests":
179
+ this.rules[name.slice(2)] = true;
180
+ break;
181
+ case "--listTests":
182
+ this.listTests = true;
183
+ break;
184
+ case "-h":
185
+ case "--help":
186
+ this.help = true;
187
+ break;
188
+ default:
189
+ if (name.startsWith("-"))
190
+ throw new Error(`Unknown option ${name}.`);
191
+ this.patterns.push(argument);
192
+ }
193
+ }
194
+ }
195
+ /** The configured rules, with the ones given on the command line instead. */
196
+ options(rules2) {
197
+ return {
198
+ ...rules2,
199
+ ...this.rules,
200
+ patterns: this.patterns,
201
+ namePattern: this.namePattern
202
+ };
203
+ }
204
+ };
205
+
206
+ // packages/testero/src/config.ts
207
+ var extensions = "{js,jsx,ts,tsx,mjs,cjs,mts,cts}";
208
+ var rules = {
209
+ testMatch: [
210
+ `**/__tests__/**/*.${extensions}`,
211
+ `**/*.{spec,test}.${extensions}`
212
+ ],
213
+ testTimeout: 5e3,
214
+ bail: false,
215
+ verbose: false,
216
+ passWithNoTests: false,
217
+ clearMocks: false,
218
+ resetMocks: false,
219
+ restoreMocks: false
220
+ };
221
+ var config_default = {
222
+ ignore: [".git", "node_modules"],
223
+ rules
224
+ };
225
+
3
226
  // packages/testero/src/testero.ts
4
- var testero_default = new class Testero {
5
- greeting;
227
+ import { join as join3 } from "node:path";
228
+
229
+ // packages/testero/src/expect/format.ts
230
+ var Format = class _Format {
231
+ /** A value with one property or item per line. */
232
+ static value(value) {
233
+ return _Format.#print(value, "", true, []);
234
+ }
235
+ /** A value on one line. */
236
+ static inline(value) {
237
+ return _Format.#print(value, "", false, []);
238
+ }
239
+ /** A line diff of two values, `-` for expected lines and `+` for received ones. */
240
+ static diff(expected, received) {
241
+ const a = _Format.value(expected).split("\n");
242
+ const b = _Format.value(received).split("\n");
243
+ if (a.length * b.length > 4e6)
244
+ return `Expected: ${_Format.inline(expected)}
245
+ Received: ${_Format.inline(received)}`;
246
+ return ["- Expected", "+ Received", "", ..._Format.#lines(a, b)].join(
247
+ "\n"
248
+ );
249
+ }
250
+ /** Lines of a longest common subsequence diff. */
251
+ static #lines(a, b) {
252
+ const lengths = Array.from(
253
+ { length: a.length + 1 },
254
+ () => new Array(b.length + 1).fill(0)
255
+ );
256
+ for (let i2 = a.length - 1; i2 >= 0; i2--)
257
+ for (let j2 = b.length - 1; j2 >= 0; j2--)
258
+ lengths[i2][j2] = a[i2] === b[j2] ? lengths[i2 + 1][j2 + 1] + 1 : Math.max(
259
+ lengths[i2 + 1][j2],
260
+ lengths[i2][j2 + 1]
261
+ );
262
+ const lines = [];
263
+ let i = 0;
264
+ let j = 0;
265
+ while (i < a.length || j < b.length) {
266
+ if (i < a.length && j < b.length && a[i] === b[j]) {
267
+ lines.push(` ${a[i++]}`);
268
+ j++;
269
+ } else if (j >= b.length || i < a.length && lengths[i + 1][j] >= lengths[i][j + 1])
270
+ lines.push(`- ${a[i++]}`);
271
+ else lines.push(`+ ${b[j++]}`);
272
+ }
273
+ return lines;
274
+ }
275
+ static #print(value, indent, multiline, seen) {
276
+ switch (typeof value) {
277
+ case "string":
278
+ return JSON.stringify(value);
279
+ case "number":
280
+ return Object.is(value, -0) ? "-0" : String(value);
281
+ case "bigint":
282
+ return `${value}n`;
283
+ case "symbol":
284
+ return value.toString();
285
+ case "function":
286
+ return `[Function ${value.name || "anonymous"}]`;
287
+ case "undefined":
288
+ return "undefined";
289
+ case "boolean":
290
+ return String(value);
291
+ }
292
+ if (value === null) return "null";
293
+ const object = value;
294
+ if (typeof object.asymmetricMatch === "function") return String(object);
295
+ if (value instanceof Date)
296
+ return Number.isNaN(value.getTime()) ? "Date { NaN }" : value.toISOString();
297
+ if (value instanceof RegExp) return String(value);
298
+ if (value instanceof Error) return `[${value.name}: ${value.message}]`;
299
+ if (seen.includes(value)) return "[Circular]";
300
+ const inner = [...seen, value];
301
+ const print = (item) => _Format.#print(item, `${indent} `, multiline, inner);
302
+ const name = object.constructor?.name;
303
+ if (value instanceof Map)
304
+ return _Format.#block(
305
+ `Map {`,
306
+ "}",
307
+ [...value].map(
308
+ ([key, item]) => `${print(key)} => ${print(item)}`
309
+ ),
310
+ indent,
311
+ multiline
312
+ );
313
+ if (value instanceof Set)
314
+ return _Format.#block(
315
+ "Set {",
316
+ "}",
317
+ [...value].map(print),
318
+ indent,
319
+ multiline
320
+ );
321
+ if (Array.isArray(value) || ArrayBuffer.isView(value)) {
322
+ const items = Array.from(value, print);
323
+ const prefix2 = name && name !== "Array" ? `${name} [` : "[";
324
+ return _Format.#block(prefix2, "]", items, indent, multiline);
325
+ }
326
+ const keys = [
327
+ ...Object.keys(object),
328
+ ...Object.getOwnPropertySymbols(object).filter(
329
+ (symbol) => Object.getOwnPropertyDescriptor(object, symbol)?.enumerable
330
+ )
331
+ ];
332
+ const entries = keys.map(
333
+ (key) => `${typeof key === "symbol" ? key.toString() : JSON.stringify(key)}: ${print(object[key])}`
334
+ );
335
+ const prefix = name && name !== "Object" ? `${name} {` : "{";
336
+ return _Format.#block(prefix, "}", entries, indent, multiline);
337
+ }
338
+ static #block(open, close, entries, indent, multiline) {
339
+ if (entries.length === 0) return `${open}${close}`;
340
+ if (!multiline) return `${open}${entries.join(", ")}${close}`;
341
+ return `${open}
342
+ ${entries.map((entry) => `${indent} ${entry},
343
+ `).join("")}${indent}${close}`;
344
+ }
345
+ };
346
+
347
+ // packages/testero/src/each.ts
348
+ var Each = class _Each {
349
+ /**
350
+ * The argument lists of a table: rows of arrays are spread, other rows
351
+ * are one argument, and a tagged template gives one object per row.
352
+ */
353
+ static rows(table, values) {
354
+ if (_Each.#template(table)) {
355
+ const names = table.join("").trim().split(/\s*\|\s*/);
356
+ const rows = [];
357
+ for (let index = 0; index < values.length; index += names.length)
358
+ rows.push([
359
+ Object.fromEntries(
360
+ names.map((name, offset) => [
361
+ name,
362
+ values[index + offset]
363
+ ])
364
+ )
365
+ ]);
366
+ return rows;
367
+ }
368
+ if (!Array.isArray(table))
369
+ throw new TypeError(
370
+ "each needs an array or a tagged template table"
371
+ );
372
+ return table.map((row) => Array.isArray(row) ? row : [row]);
373
+ }
374
+ /** A title with `$name` properties of an object row, or printf placeholders. */
375
+ static title(title, args, index) {
376
+ const [first] = args;
377
+ if (args.length === 1 && _Each.#plain(first) && title.includes("$"))
378
+ return title.replace(
379
+ /\$([\w$]+(?:\.[\w$]+)*)/g,
380
+ (match, path) => path === "#" ? String(index) : _Each.#show(_Each.#at(first, path.split("."), match))
381
+ );
382
+ const rest = [...args];
383
+ return title.replace(/%([sdifjoOp#$%])/g, (match, flag) => {
384
+ if (flag === "%") return "%";
385
+ if (flag === "#") return String(index);
386
+ if (flag === "$") return String(index + 1);
387
+ if (rest.length === 0) return match;
388
+ const value = rest.shift();
389
+ switch (flag) {
390
+ case "d":
391
+ case "i":
392
+ return String(
393
+ flag === "i" ? Math.trunc(Number(value)) : Number(value)
394
+ );
395
+ case "f":
396
+ return String(Number(value));
397
+ case "j":
398
+ return JSON.stringify(value);
399
+ case "s":
400
+ return _Each.#show(value);
401
+ default:
402
+ return Format.inline(value);
403
+ }
404
+ });
405
+ }
406
+ static #template(table) {
407
+ return Array.isArray(table) && "raw" in table;
408
+ }
409
+ static #plain(value) {
410
+ return typeof value === "object" && value !== null && !Array.isArray(value);
411
+ }
412
+ static #at(value, path, fallback) {
413
+ let current = value;
414
+ for (const key of path) {
415
+ if (typeof current !== "object" || current === null || !(key in current))
416
+ return fallback;
417
+ current = current[key];
418
+ }
419
+ return current;
420
+ }
421
+ /** Strings as they are, other values like Jest prints them. */
422
+ static #show(value) {
423
+ return typeof value === "string" ? value : Format.inline(value);
424
+ }
425
+ };
426
+
427
+ // packages/testero/src/suite.ts
428
+ var Suite = class {
429
+ name;
430
+ mode;
431
+ parent;
432
+ children = [];
433
+ hooks = {
434
+ beforeAll: [],
435
+ afterAll: [],
436
+ beforeEach: [],
437
+ afterEach: []
438
+ };
439
+ constructor(name, mode = "run", parent) {
440
+ this.name = name;
441
+ this.mode = mode;
442
+ this.parent = parent;
443
+ }
444
+ /** The names of the suites from the file down to this one, the file has none. */
445
+ get titles() {
446
+ return this.parent ? [...this.parent.titles, this.name] : [];
447
+ }
448
+ /** This suite and the ones around it, from the file down. */
449
+ get path() {
450
+ return this.parent ? [...this.parent.path, this] : [this];
451
+ }
452
+ /** Every test, of nested suites too, in order. */
453
+ *tests() {
454
+ for (const child of this.children)
455
+ if (child instanceof Test) yield child;
456
+ else yield* child.tests();
457
+ }
458
+ };
459
+ var Test = class {
460
+ name;
461
+ body;
462
+ mode;
463
+ timeout;
464
+ suite;
465
+ constructor(name, body, mode, timeout, suite) {
466
+ this.name = name;
467
+ this.body = body;
468
+ this.mode = mode;
469
+ this.timeout = timeout;
470
+ this.suite = suite;
471
+ }
472
+ /** The names of the suites around the test, and its own. */
473
+ get titles() {
474
+ return [...this.suite.titles, this.name];
475
+ }
476
+ /** The titles joined like Jest does for `-t`. */
477
+ get fullName() {
478
+ return this.titles.join(" ");
479
+ }
480
+ /** Whether the test, or a suite around it, is marked with `only`. */
481
+ get focused() {
482
+ return this.mode === "only" || this.suite.path.some((suite) => suite.mode === "only");
483
+ }
484
+ /** Whether the test, or a suite around it, is skipped or todo. */
485
+ get skipped() {
486
+ return this.mode === "skip" || this.mode === "todo" || this.suite.path.some((suite) => suite.mode === "skip");
487
+ }
488
+ };
489
+
490
+ // packages/testero/src/collector.ts
491
+ var Collector = class {
492
+ root = new Suite("");
493
+ #current = this.root;
494
+ #closed = false;
495
+ describe = this.#describes();
496
+ test = this.#tests();
497
+ /** Ends collection, tests cannot be declared while tests run. */
498
+ close() {
499
+ this.#closed = true;
500
+ }
501
+ /** `beforeAll`, `afterAll`, `beforeEach` or `afterEach`. */
502
+ hook = (kind) => (body, timeout) => {
503
+ this.#open(kind);
504
+ this.#current.hooks[kind].push({ body, timeout });
505
+ };
506
+ #describes() {
507
+ const describe = (mode) => (name, body) => this.#describe(name, body, mode);
508
+ const each = (mode) => (table, ...values) => (name, body) => {
509
+ for (const [index, row] of Each.rows(table, values).entries())
510
+ this.#describe(
511
+ Each.title(String(name), row, index),
512
+ () => body(...row),
513
+ mode
514
+ );
515
+ };
516
+ return Object.assign(describe("run"), {
517
+ only: Object.assign(describe("only"), { each: each("only") }),
518
+ skip: Object.assign(describe("skip"), { each: each("skip") }),
519
+ each: each("run")
520
+ });
521
+ }
522
+ #tests() {
523
+ const test = (mode) => (name, body, timeout) => this.#test(name, body, mode, timeout);
524
+ const each = (mode) => (table, ...values) => (name, body, timeout) => {
525
+ for (const [index, row] of Each.rows(table, values).entries())
526
+ this.#test(
527
+ Each.title(String(name), row, index),
528
+ () => body(...row),
529
+ mode,
530
+ timeout
531
+ );
532
+ };
533
+ return Object.assign(test("run"), {
534
+ only: Object.assign(test("only"), { each: each("only") }),
535
+ skip: Object.assign(test("skip"), { each: each("skip") }),
536
+ todo: (name) => this.#test(name, void 0, "todo", void 0),
537
+ each: each("run")
538
+ });
539
+ }
540
+ #describe(name, body, mode) {
541
+ this.#open("describe");
542
+ const suite = new Suite(String(name), mode, this.#current);
543
+ this.#current.children.push(suite);
544
+ const outer = this.#current;
545
+ this.#current = suite;
546
+ try {
547
+ const result = body();
548
+ if (result instanceof Promise)
549
+ throw new Error(
550
+ `describe "${suite.name}" returned a promise, its tests must be declared synchronously.`
551
+ );
552
+ } finally {
553
+ this.#current = outer;
554
+ }
555
+ }
556
+ #test(name, body, mode, timeout) {
557
+ this.#open("test");
558
+ if (mode !== "todo" && typeof body !== "function")
559
+ throw new TypeError(`test "${name}" needs a function.`);
560
+ this.#current.children.push(
561
+ new Test(String(name), body, mode, timeout, this.#current)
562
+ );
563
+ }
564
+ #open(what) {
565
+ if (this.#closed)
566
+ throw new Error(
567
+ `Cannot declare ${what} while tests run, declare it at the top level or in describe.`
568
+ );
569
+ }
570
+ };
571
+
572
+ // packages/testero/src/mock.ts
573
+ var Mock = class _Mock {
574
+ function;
575
+ state = _Mock.#empty();
576
+ /** What the mock does after a reset, a spy calls its original. */
577
+ #original;
578
+ #restore;
579
+ #implementation;
580
+ #once = [];
581
+ #name = "testero.fn()";
582
+ constructor(implementation, restore) {
583
+ this.#original = implementation;
584
+ this.#implementation = implementation;
585
+ this.#restore = restore;
586
+ const mock = this;
587
+ const fn = function(...args) {
588
+ return mock.#call(this, args, new.target);
589
+ };
590
+ Object.defineProperty(fn, "mock", { get: () => mock.state });
591
+ this.function = Object.assign(fn, this.#methods(fn));
592
+ }
593
+ #methods(fn) {
594
+ const set = (change) => {
595
+ change();
596
+ return fn;
597
+ };
598
+ return {
599
+ _isMockFunction: true,
600
+ mockImplementation: (implementation) => set(() => {
601
+ this.#implementation = implementation;
602
+ }),
603
+ mockImplementationOnce: (implementation) => set(() => this.#once.push(implementation)),
604
+ mockReturnValue: (value) => fn.mockImplementation(() => value),
605
+ mockReturnValueOnce: (value) => fn.mockImplementationOnce(() => value),
606
+ mockResolvedValue: (value) => fn.mockImplementation(() => Promise.resolve(value)),
607
+ mockResolvedValueOnce: (value) => fn.mockImplementationOnce(() => Promise.resolve(value)),
608
+ mockRejectedValue: (value) => fn.mockImplementation(() => Promise.reject(value)),
609
+ mockRejectedValueOnce: (value) => fn.mockImplementationOnce(() => Promise.reject(value)),
610
+ mockReturnThis: () => fn.mockImplementation(function() {
611
+ return this;
612
+ }),
613
+ mockName: (name) => set(() => {
614
+ this.#name = name;
615
+ }),
616
+ getMockName: () => this.#name,
617
+ getMockImplementation: () => this.#implementation,
618
+ mockClear: () => set(() => this.clear()),
619
+ mockReset: () => set(() => this.reset()),
620
+ mockRestore: () => this.restore()
621
+ };
622
+ }
623
+ /** Forgets the calls. */
624
+ clear() {
625
+ this.state = _Mock.#empty();
626
+ }
627
+ /** Forgets the calls and the implementations given since. */
628
+ reset() {
629
+ this.clear();
630
+ this.#implementation = this.#original;
631
+ this.#once = [];
632
+ }
633
+ /** Resets, and puts a spied method back. */
634
+ restore() {
635
+ this.reset();
636
+ this.#restore?.();
637
+ }
638
+ #call(context, args, target) {
639
+ const result = { type: "incomplete", value: void 0 };
640
+ this.state.calls.push(args);
641
+ this.state.contexts.push(context);
642
+ this.state.instances.push(context);
643
+ this.state.results.push(result);
644
+ this.state.lastCall = args;
645
+ const implementation = this.#once.shift() ?? this.#implementation;
646
+ try {
647
+ result.value = !implementation ? void 0 : target ? Reflect.construct(implementation, args, target) : implementation.apply(context, args);
648
+ result.type = "return";
649
+ return result.value;
650
+ } catch (error) {
651
+ result.type = "throw";
652
+ result.value = error;
653
+ throw error;
654
+ }
655
+ }
656
+ static #empty() {
657
+ return {
658
+ calls: [],
659
+ results: [],
660
+ instances: [],
661
+ contexts: [],
662
+ lastCall: void 0
663
+ };
664
+ }
665
+ };
666
+ var Mocks = class _Mocks {
667
+ static #all = /* @__PURE__ */ new Map();
668
+ /** A mock function, doing what an implementation does, if given. */
669
+ static fn(implementation) {
670
+ const mock = new Mock(implementation);
671
+ _Mocks.#all.set(mock.function, mock);
672
+ return mock.function;
673
+ }
674
+ /** Replaces a method with a mock that calls it, until restored. */
675
+ static spyOn(object, method) {
676
+ const original = object[method];
677
+ if (_Mocks.isMockFunction(original)) return original;
678
+ if (typeof original !== "function")
679
+ throw new TypeError(
680
+ `Cannot spy on ${String(method)}, it is ${typeof original}, not a function.`
681
+ );
682
+ const own = Object.hasOwn(object, method);
683
+ const mock = new Mock(
684
+ function(...args) {
685
+ return original.apply(this, args);
686
+ },
687
+ () => {
688
+ if (own) object[method] = original;
689
+ else delete object[method];
690
+ }
691
+ );
692
+ object[method] = mock.function;
693
+ _Mocks.#all.set(mock.function, mock);
694
+ return mock.function;
695
+ }
696
+ static isMockFunction(value) {
697
+ return typeof value === "function" && value._isMockFunction === true;
698
+ }
699
+ static clearAll() {
700
+ for (const mock of _Mocks.#all.values()) mock.clear();
701
+ }
702
+ static resetAll() {
703
+ for (const mock of _Mocks.#all.values()) mock.reset();
704
+ }
705
+ static restoreAll() {
706
+ for (const mock of _Mocks.#all.values()) mock.restore();
707
+ }
708
+ /** Forgets every mock, between test files. */
709
+ static forget() {
710
+ _Mocks.#all.clear();
711
+ }
712
+ };
713
+
714
+ // packages/testero/src/expect/equality.ts
715
+ var Equality = class _Equality {
716
+ /** Whether a value matches by its own rule, like `expect.any(Number)`. */
717
+ static asymmetric(value) {
718
+ return typeof value === "object" && value !== null && typeof value.asymmetricMatch === "function";
719
+ }
720
+ /**
721
+ * Whether two values are deeply equal. Loosely, properties set to
722
+ * undefined count as missing; strictly, types and holes in arrays count.
723
+ */
724
+ static equals(a, b, strict = false) {
725
+ return _Equality.#equals(a, b, strict, [], []);
726
+ }
727
+ /** Whether received has every property expected has, recursively, like `toMatchObject`. */
728
+ static matches(received, expected) {
729
+ if (_Equality.asymmetric(expected))
730
+ return expected.asymmetricMatch(received);
731
+ if (!_Equality.#plainish(expected) || typeof received !== "object" || received === null)
732
+ return _Equality.equals(received, expected);
733
+ if (Array.isArray(expected))
734
+ return Array.isArray(received) && received.length === expected.length && expected.every(
735
+ (item, index) => _Equality.matches(received[index], item)
736
+ );
737
+ return _Equality.#keys(expected, true).every(
738
+ (key) => key in received && _Equality.matches(
739
+ received[key],
740
+ expected[key]
741
+ )
742
+ );
743
+ }
744
+ /** Objects and arrays that `toMatchObject` compares by their properties. */
745
+ static #plainish(value) {
746
+ return typeof value === "object" && value !== null && !(value instanceof Date) && !(value instanceof RegExp) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Error);
747
+ }
748
+ static #equals(a, b, strict, aSeen, bSeen) {
749
+ if (_Equality.asymmetric(a)) return a.asymmetricMatch(b);
750
+ if (_Equality.asymmetric(b)) return b.asymmetricMatch(a);
751
+ if (Object.is(a, b)) return true;
752
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
753
+ return false;
754
+ const tag = Object.prototype.toString.call(a);
755
+ if (tag !== Object.prototype.toString.call(b)) return false;
756
+ if (strict && Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
757
+ return false;
758
+ if (a instanceof Date)
759
+ return Object.is(a.getTime(), b.getTime());
760
+ if (a instanceof RegExp) return String(a) === String(b);
761
+ if (a instanceof Error && b instanceof Error)
762
+ return a.name === b.name && a.message === b.message;
763
+ if (a instanceof Number || a instanceof String || a instanceof Boolean)
764
+ return Object.is(
765
+ a.valueOf(),
766
+ b.valueOf()
767
+ );
768
+ const index = aSeen.indexOf(a);
769
+ if (index >= 0) return bSeen[index] === b;
770
+ aSeen.push(a);
771
+ bSeen.push(b);
772
+ try {
773
+ return _Equality.#structures(a, b, strict, aSeen, bSeen);
774
+ } finally {
775
+ aSeen.pop();
776
+ bSeen.pop();
777
+ }
778
+ }
779
+ static #structures(a, b, strict, aSeen, bSeen) {
780
+ const equal = (x, y) => _Equality.#equals(x, y, strict, aSeen, bSeen);
781
+ if (a instanceof Map && b instanceof Map)
782
+ return a.size === b.size && [...a].every(
783
+ ([key, value]) => b.has(key) ? equal(value, b.get(key)) : [...b].some(
784
+ ([other, item]) => equal(key, other) && equal(value, item)
785
+ )
786
+ );
787
+ if (a instanceof Set && b instanceof Set)
788
+ return a.size === b.size && [...a].every(
789
+ (value) => b.has(value) || [...b].some((other) => equal(value, other))
790
+ );
791
+ if (Array.isArray(a) || ArrayBuffer.isView(a)) {
792
+ const left = a;
793
+ const right = b;
794
+ if (left.length !== right.length) return false;
795
+ for (let index = 0; index < left.length; index++) {
796
+ if (strict && index in left !== index in right) return false;
797
+ if (!equal(left[index], right[index])) return false;
798
+ }
799
+ }
800
+ const indexed = Array.isArray(a) || ArrayBuffer.isView(a);
801
+ const keysOf = (object) => _Equality.#keys(object, strict).filter(
802
+ (key) => !(indexed && typeof key === "string" && /^\d+$/.test(key))
803
+ );
804
+ const aKeys = keysOf(a);
805
+ const bKeys = keysOf(b);
806
+ if (aKeys.length !== bKeys.length) return false;
807
+ return aKeys.every(
808
+ (key) => Object.hasOwn(b, key) && equal(
809
+ a[key],
810
+ b[key]
811
+ )
812
+ );
813
+ }
814
+ /** Own enumerable keys, without the ones set to undefined unless strict. */
815
+ static #keys(object, strict) {
816
+ const record = object;
817
+ const keys = [
818
+ ...Object.keys(object),
819
+ ...Object.getOwnPropertySymbols(object).filter(
820
+ (symbol) => Object.getOwnPropertyDescriptor(object, symbol)?.enumerable
821
+ )
822
+ ];
823
+ return strict ? keys : keys.filter((key) => record[key] !== void 0);
824
+ }
825
+ };
826
+
827
+ // packages/testero/src/expect/matchers.ts
828
+ var show = (value) => Format.inline(value);
829
+ var hint = (context, name, expected = "expected") => `expect(received)${context.promise ? `.${context.promise}` : ""}${context.isNot ? ".not" : ""}.${name}(${expected})`;
830
+ var difference = (expected, received) => {
831
+ const multiline = Format.value(expected).includes("\n") || Format.value(received).includes("\n");
832
+ return multiline ? Format.diff(expected, received) : `Expected: ${show(expected)}
833
+ Received: ${show(received)}`;
834
+ };
835
+ var callsOf = (context, name, received) => {
836
+ if (!Mocks.isMockFunction(received))
837
+ throw new Error(
838
+ `${hint(context, name)}
839
+
840
+ Matcher error: received value must be a mock or spy function
841
+
842
+ Received: ${show(received)}`
843
+ );
844
+ return received.mock;
845
+ };
846
+ var listCalls = (calls) => calls.length === 0 ? "Number of calls: 0" : `Received
847
+ ${calls.map(
848
+ (call, index) => ` ${index + 1}: ${call.map(show).join(", ")}`
849
+ ).join("\n")}
850
+
851
+ Number of calls: ${calls.length}`;
852
+ var comparison = (name, operator, test) => function(received, expected) {
853
+ if (typeof received !== "number" && typeof received !== "bigint")
854
+ throw new Error(
855
+ `${hint(this, name)}
856
+
857
+ Matcher error: received value must be a number or bigint
858
+
859
+ Received: ${show(received)}`
860
+ );
861
+ return {
862
+ pass: test(received, expected),
863
+ message: () => `${hint(this, name)}
864
+
865
+ Expected:${this.isNot ? " not" : ""} ${operator} ${show(expected)}
866
+ Received:${" ".repeat(operator.length + (this.isNot ? 6 : 2))}${show(received)}`
867
+ };
868
+ };
869
+ var predicate = (name, test) => function(received) {
870
+ return {
871
+ pass: test(received),
872
+ message: () => `${hint(this, name, "")}
873
+
874
+ Received: ${show(received)}`
875
+ };
876
+ };
877
+ var thrownMatch = (expected, thrown) => {
878
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
879
+ const received = `Received message: ${JSON.stringify(message)}`;
880
+ if (expected === void 0) return { pass: true, expected: "", received };
881
+ if (typeof expected === "string")
882
+ return {
883
+ pass: message.includes(expected),
884
+ expected: `Expected substring: ${JSON.stringify(expected)}`,
885
+ received
886
+ };
887
+ if (expected instanceof RegExp)
888
+ return {
889
+ pass: expected.test(message),
890
+ expected: `Expected pattern: ${expected}`,
891
+ received
892
+ };
893
+ if (typeof expected === "function")
894
+ return {
895
+ pass: thrown instanceof expected,
896
+ expected: `Expected constructor: ${expected.name}`,
897
+ received: `Received constructor: ${thrown?.constructor?.name ?? show(thrown)}
898
+ ${received}`
899
+ };
900
+ if (Equality.asymmetric(expected))
901
+ return {
902
+ pass: expected.asymmetricMatch(thrown),
903
+ expected: `Expected asymmetric matcher: ${String(expected)}`,
904
+ received: `Received value: ${show(thrown)}`
905
+ };
906
+ if (expected instanceof Error)
907
+ return {
908
+ pass: message === expected.message,
909
+ expected: `Expected message: ${JSON.stringify(expected.message)}`,
910
+ received
911
+ };
912
+ throw new Error(
913
+ `Matcher error: expected value must be a string, regular expression, class, error or asymmetric matcher
914
+
915
+ Expected: ${show(expected)}`
916
+ );
917
+ };
918
+ var property = (object, path) => {
919
+ const keys = typeof path === "string" ? path.split(/[.[\]]/).filter((key) => key !== "") : path;
920
+ let current = object;
921
+ const found = [];
922
+ for (const key of keys) {
923
+ if (current === null || current === void 0 || !(key in Object(current)))
924
+ return { has: false, value: current, found };
925
+ current = current[key];
926
+ found.push(key);
927
+ }
928
+ return { has: true, value: current, found };
929
+ };
930
+ var matchers = {
931
+ toBe(received, expected) {
932
+ const pass = Object.is(received, expected);
933
+ return {
934
+ pass,
935
+ message: () => {
936
+ const header = `${hint(this, "toBe")} // Object.is equality
937
+
938
+ `;
939
+ if (this.isNot)
940
+ return `${header}Expected: not ${show(expected)}`;
941
+ const deep = Equality.equals(received, expected, true) ? '\n\nIf it should pass with deep equality, replace "toBe" with "toStrictEqual"' : "";
942
+ return `${header}${difference(expected, received)}${deep}`;
943
+ }
944
+ };
945
+ },
946
+ toEqual(received, expected) {
947
+ return {
948
+ pass: Equality.equals(received, expected),
949
+ message: () => `${hint(this, "toEqual")} // deep equality
950
+
951
+ ${this.isNot ? `Expected: not ${show(expected)}` : difference(expected, received)}`
952
+ };
953
+ },
954
+ toStrictEqual(received, expected) {
955
+ return {
956
+ pass: Equality.equals(received, expected, true),
957
+ message: () => `${hint(this, "toStrictEqual")} // deep equality
958
+
959
+ ${this.isNot ? `Expected: not ${show(expected)}` : difference(expected, received)}`
960
+ };
961
+ },
962
+ toMatchObject(received, expected) {
963
+ if (typeof received !== "object" || received === null)
964
+ throw new Error(
965
+ `${hint(this, "toMatchObject")}
966
+
967
+ Matcher error: received value must be a non-null object
968
+
969
+ Received: ${show(received)}`
970
+ );
971
+ return {
972
+ pass: Equality.matches(received, expected),
973
+ message: () => `${hint(this, "toMatchObject")}
974
+
975
+ ${this.isNot ? `Expected: not ${show(expected)}` : difference(expected, received)}`
976
+ };
977
+ },
978
+ toBeDefined: predicate("toBeDefined", (value) => value !== void 0),
979
+ toBeUndefined: predicate("toBeUndefined", (value) => value === void 0),
980
+ toBeNull: predicate("toBeNull", (value) => value === null),
981
+ toBeNaN: predicate("toBeNaN", (value) => Number.isNaN(value)),
982
+ toBeTruthy: predicate("toBeTruthy", (value) => Boolean(value)),
983
+ toBeFalsy: predicate("toBeFalsy", (value) => !value),
984
+ toBeGreaterThan: comparison("toBeGreaterThan", ">", (a, b) => a > b),
985
+ toBeGreaterThanOrEqual: comparison(
986
+ "toBeGreaterThanOrEqual",
987
+ ">=",
988
+ (a, b) => a >= b
989
+ ),
990
+ toBeLessThan: comparison("toBeLessThan", "<", (a, b) => a < b),
991
+ toBeLessThanOrEqual: comparison(
992
+ "toBeLessThanOrEqual",
993
+ "<=",
994
+ (a, b) => a <= b
995
+ ),
996
+ toBeCloseTo(received, expected, digits = 2) {
997
+ const value = received;
998
+ const pass = value === expected || Math.abs(expected - value) < 10 ** -digits / 2;
999
+ return {
1000
+ pass,
1001
+ message: () => `${hint(this, "toBeCloseTo", "expected, precision")}
1002
+
1003
+ Expected: ${this.isNot ? "not " : ""}${show(expected)}
1004
+ Received: ${show(received)}
1005
+
1006
+ Expected precision: ${digits}
1007
+ Expected difference: < ${10 ** -digits / 2}
1008
+ Received difference: ${Math.abs(expected - value)}`
1009
+ };
1010
+ },
1011
+ toBeInstanceOf(received, expected) {
1012
+ return {
1013
+ pass: received instanceof expected,
1014
+ message: () => `${hint(this, "toBeInstanceOf")}
1015
+
1016
+ Expected constructor: ${this.isNot ? "not " : ""}${expected.name}
1017
+ Received constructor: ${received?.constructor?.name ?? show(received)}`
1018
+ };
1019
+ },
1020
+ toContain(received, expected) {
1021
+ const text = typeof received === "string";
1022
+ if (!text && (received === null || typeof received?.[Symbol.iterator] !== "function"))
1023
+ throw new Error(
1024
+ `${hint(this, "toContain")}
1025
+
1026
+ Matcher error: received value must not be null nor undefined, and must be iterable
1027
+
1028
+ Received: ${show(received)}`
1029
+ );
1030
+ const pass = text ? received.includes(String(expected)) : [...received].includes(expected);
1031
+ return {
1032
+ pass,
1033
+ message: () => `${hint(this, "toContain")}
1034
+
1035
+ Expected ${text ? "substring" : "value"}: ${this.isNot ? "not " : ""}${show(expected)}
1036
+ Received ${text ? "string" : "value"}:${text ? " " : " "}${show(received)}`
1037
+ };
1038
+ },
1039
+ toContainEqual(received, expected) {
1040
+ const items = [...received];
1041
+ return {
1042
+ pass: items.some((item) => Equality.equals(item, expected)),
1043
+ message: () => `${hint(this, "toContainEqual")}
1044
+
1045
+ Expected value: ${this.isNot ? "not " : ""}${show(expected)}
1046
+ Received value: ${show(received)}`
1047
+ };
1048
+ },
1049
+ toHaveLength(received, expected) {
1050
+ const length = received?.length;
1051
+ if (typeof length !== "number")
1052
+ throw new Error(
1053
+ `${hint(this, "toHaveLength")}
1054
+
1055
+ Matcher error: received value must have a length property whose value must be a number
1056
+
1057
+ Received: ${show(received)}`
1058
+ );
1059
+ return {
1060
+ pass: length === expected,
1061
+ message: () => `${hint(this, "toHaveLength")}
1062
+
1063
+ Expected length: ${this.isNot ? "not " : ""}${expected}
1064
+ Received length: ${length}
1065
+ Received value: ${show(received)}`
1066
+ };
1067
+ },
1068
+ toHaveProperty(received, path, ...value) {
1069
+ const { has, value: actual, found } = property(received, path);
1070
+ const checksValue = value.length > 0;
1071
+ const pass = has && (!checksValue || Equality.equals(actual, value[0]));
1072
+ const shown = typeof path === "string" ? path : path.map(String).join(".");
1073
+ return {
1074
+ pass,
1075
+ message: () => {
1076
+ const header = `${hint(this, "toHaveProperty", checksValue ? "path, value" : "path")}
1077
+
1078
+ Expected path: ${JSON.stringify(shown)}
1079
+ `;
1080
+ if (!has)
1081
+ return `${header}Received path: ${JSON.stringify(found.map(String).join("."))}
1082
+
1083
+ Received value: ${show(actual)}`;
1084
+ return checksValue ? `${header}
1085
+ ${this.isNot ? `Expected value: not ${show(value[0])}` : difference(value[0], actual)}` : `${header}
1086
+ Received value: ${show(actual)}`;
1087
+ }
1088
+ };
1089
+ },
1090
+ toMatch(received, expected) {
1091
+ if (typeof received !== "string")
1092
+ throw new Error(
1093
+ `${hint(this, "toMatch")}
1094
+
1095
+ Matcher error: received value must be a string
1096
+
1097
+ Received: ${show(received)}`
1098
+ );
1099
+ const pattern = typeof expected === "string";
1100
+ return {
1101
+ pass: pattern ? received.includes(expected) : expected.test(received),
1102
+ message: () => `${hint(this, "toMatch")}
1103
+
1104
+ Expected ${pattern ? "substring" : "pattern"}: ${this.isNot ? "not " : ""}${pattern ? show(expected) : String(expected)}
1105
+ Received string:${pattern ? " " : " "}${show(received)}`
1106
+ };
1107
+ },
1108
+ toThrow(received, expected) {
1109
+ let thrown;
1110
+ if (this.promise === "rejects") thrown = { value: received };
1111
+ else if (typeof received !== "function")
1112
+ throw new Error(
1113
+ `${hint(this, "toThrow")}
1114
+
1115
+ Matcher error: received value must be a function
1116
+
1117
+ Received: ${show(received)}`
1118
+ );
1119
+ else
1120
+ try {
1121
+ received();
1122
+ } catch (error) {
1123
+ thrown = { value: error };
1124
+ }
1125
+ const match = thrown ? thrownMatch(expected, thrown.value) : void 0;
1126
+ const name = expected === void 0 ? "" : "expected";
1127
+ return {
1128
+ pass: match?.pass ?? false,
1129
+ message: () => {
1130
+ const header = `${hint(this, "toThrow", name)}
1131
+
1132
+ `;
1133
+ if (!thrown || !match)
1134
+ return `${header}${expected === void 0 ? "" : `${thrownMatch(expected, void 0).expected}
1135
+
1136
+ `}Received function did not throw`;
1137
+ if (this.isNot && expected === void 0)
1138
+ return `${header}Error name: ${thrown.value?.name ?? "thrown"}
1139
+ ${match.received}`;
1140
+ return `${header}${match.expected.replace(": ", this.isNot ? ": not " : ": ")}
1141
+ ${match.received}`;
1142
+ }
1143
+ };
1144
+ },
1145
+ toHaveBeenCalled(received) {
1146
+ const { calls } = callsOf(this, "toHaveBeenCalled", received);
1147
+ return {
1148
+ pass: calls.length > 0,
1149
+ message: () => `${hint(this, "toHaveBeenCalled", "")}
1150
+
1151
+ Expected number of calls: ${this.isNot ? "0" : ">= 1"}
1152
+ ${listCalls(calls)}`
1153
+ };
1154
+ },
1155
+ toHaveBeenCalledTimes(received, expected) {
1156
+ const { calls } = callsOf(this, "toHaveBeenCalledTimes", received);
1157
+ return {
1158
+ pass: calls.length === expected,
1159
+ message: () => `${hint(this, "toHaveBeenCalledTimes")}
1160
+
1161
+ Expected number of calls: ${this.isNot ? "not " : ""}${expected}
1162
+ Received number of calls: ${calls.length}`
1163
+ };
1164
+ },
1165
+ toHaveBeenCalledWith(received, ...expected) {
1166
+ const { calls } = callsOf(this, "toHaveBeenCalledWith", received);
1167
+ return {
1168
+ pass: calls.some((call) => Equality.equals(call, expected)),
1169
+ message: () => `${hint(this, "toHaveBeenCalledWith", "...expected")}
1170
+
1171
+ Expected: ${this.isNot ? "not " : ""}${expected.map(show).join(", ")}
1172
+ ${listCalls(calls)}`
1173
+ };
1174
+ },
1175
+ toHaveBeenLastCalledWith(received, ...expected) {
1176
+ const { calls } = callsOf(this, "toHaveBeenLastCalledWith", received);
1177
+ const last = calls.at(-1);
1178
+ return {
1179
+ pass: last !== void 0 && Equality.equals(last, expected),
1180
+ message: () => `${hint(this, "toHaveBeenLastCalledWith", "...expected")}
1181
+
1182
+ Expected: ${this.isNot ? "not " : ""}${expected.map(show).join(", ")}
1183
+ ${listCalls(calls)}`
1184
+ };
1185
+ },
1186
+ toHaveBeenNthCalledWith(received, nth, ...expected) {
1187
+ const { calls } = callsOf(this, "toHaveBeenNthCalledWith", received);
1188
+ const call = calls[nth - 1];
1189
+ return {
1190
+ pass: call !== void 0 && Equality.equals(call, expected),
1191
+ message: () => `${hint(this, "toHaveBeenNthCalledWith", "n, ...expected")}
1192
+
1193
+ n: ${nth}
1194
+ Expected: ${this.isNot ? "not " : ""}${expected.map(show).join(", ")}
1195
+ ${listCalls(calls)}`
1196
+ };
1197
+ },
1198
+ toHaveReturned(received) {
1199
+ const { results } = callsOf(this, "toHaveReturned", received);
1200
+ const returns = results.filter((result) => result.type === "return");
1201
+ return {
1202
+ pass: returns.length > 0,
1203
+ message: () => `${hint(this, "toHaveReturned", "")}
1204
+
1205
+ Expected number of returns: ${this.isNot ? "0" : ">= 1"}
1206
+ Received number of returns: ${returns.length}`
1207
+ };
1208
+ },
1209
+ toHaveReturnedTimes(received, expected) {
1210
+ const { results } = callsOf(this, "toHaveReturnedTimes", received);
1211
+ const returns = results.filter((result) => result.type === "return");
1212
+ return {
1213
+ pass: returns.length === expected,
1214
+ message: () => `${hint(this, "toHaveReturnedTimes")}
1215
+
1216
+ Expected number of returns: ${this.isNot ? "not " : ""}${expected}
1217
+ Received number of returns: ${returns.length}`
1218
+ };
1219
+ },
1220
+ toHaveReturnedWith(received, expected) {
1221
+ const { results } = callsOf(this, "toHaveReturnedWith", received);
1222
+ return {
1223
+ pass: results.some(
1224
+ (result) => result.type === "return" && Equality.equals(result.value, expected)
1225
+ ),
1226
+ message: () => `${hint(this, "toHaveReturnedWith")}
1227
+
1228
+ Expected: ${this.isNot ? "not " : ""}${show(expected)}
1229
+ Received: ${results.map((result) => show(result.value)).join(", ")}`
1230
+ };
1231
+ },
1232
+ toHaveLastReturnedWith(received, expected) {
1233
+ const { results } = callsOf(this, "toHaveLastReturnedWith", received);
1234
+ const last = results.at(-1);
1235
+ return {
1236
+ pass: last?.type === "return" && Equality.equals(last.value, expected),
1237
+ message: () => `${hint(this, "toHaveLastReturnedWith")}
1238
+
1239
+ Expected: ${this.isNot ? "not " : ""}${show(expected)}
1240
+ Received: ${last ? show(last.value) : "no calls"}`
1241
+ };
1242
+ },
1243
+ toHaveNthReturnedWith(received, nth, expected) {
1244
+ const { results } = callsOf(this, "toHaveNthReturnedWith", received);
1245
+ const result = results[nth - 1];
1246
+ return {
1247
+ pass: result?.type === "return" && Equality.equals(result.value, expected),
1248
+ message: () => `${hint(this, "toHaveNthReturnedWith", "n, expected")}
1249
+
1250
+ n: ${nth}
1251
+ Expected: ${this.isNot ? "not " : ""}${show(expected)}
1252
+ Received: ${result ? show(result.value) : "no call"}`
1253
+ };
1254
+ }
1255
+ };
1256
+
1257
+ // packages/testero/src/expect/assertion.ts
1258
+ var AssertionError = class extends Error {
1259
+ name = "AssertionError";
1260
+ };
1261
+ var Assertion = class _Assertion {
1262
+ /** Assertions made since the last reset, for `expect.assertions(n)`. */
1263
+ static count = 0;
1264
+ #received;
1265
+ #context;
1266
+ constructor(received, context = { isNot: false, promise: "" }) {
1267
+ this.#received = received;
1268
+ this.#context = context;
1269
+ }
1270
+ /** The same assertion, expecting the opposite. */
1271
+ get not() {
1272
+ return new _Assertion(this.#received, {
1273
+ ...this.#context,
1274
+ isNot: !this.#context.isNot
1275
+ });
1276
+ }
1277
+ /** The assertion on what a promise resolves to, its matchers return promises. */
1278
+ get resolves() {
1279
+ return new _Assertion(this.#received, {
1280
+ ...this.#context,
1281
+ promise: "resolves"
1282
+ });
1283
+ }
1284
+ /** The assertion on why a promise rejects, its matchers return promises. */
1285
+ get rejects() {
1286
+ return new _Assertion(this.#received, {
1287
+ ...this.#context,
1288
+ promise: "rejects"
1289
+ });
1290
+ }
1291
+ /** Adds a matcher to every assertion, like `expect.extend`. */
1292
+ static define(name, matcher) {
1293
+ matchers[name] = matcher;
1294
+ Object.defineProperty(_Assertion.prototype, name, {
1295
+ configurable: true,
1296
+ writable: true,
1297
+ value: function check(...args) {
1298
+ return this.#run(name, args, check);
1299
+ }
1300
+ });
1301
+ }
1302
+ /** Runs a matcher now, or once the promise settles for `.resolves` and `.rejects`. */
1303
+ #run(name, args, caller) {
1304
+ _Assertion.count++;
1305
+ const origin = {};
1306
+ Error.captureStackTrace(origin, caller);
1307
+ const { promise } = this.#context;
1308
+ if (!promise) return this.#check(name, this.#received, args, origin);
1309
+ const settled = (value, resolved) => {
1310
+ if (resolved === (promise === "resolves"))
1311
+ return this.#check(name, value, args, origin);
1312
+ throw _Assertion.#error(
1313
+ `expect(received).${promise}.${name}()
1314
+
1315
+ Received promise ${resolved ? "resolved" : "rejected"} instead of ${resolved ? "rejected" : "resolved"}
1316
+ ${resolved ? "Resolved" : "Rejected"} to value: ${String(value)}`,
1317
+ origin
1318
+ );
1319
+ };
1320
+ if (!(this.#received instanceof Promise) && typeof this.#received?.then !== "function")
1321
+ throw _Assertion.#error(
1322
+ `expect(received).${promise}.${name}()
1323
+
1324
+ Matcher error: received value must be a promise`,
1325
+ origin
1326
+ );
1327
+ return Promise.resolve(this.#received).then(
1328
+ (value) => settled(value, true),
1329
+ (reason) => settled(reason, false)
1330
+ );
1331
+ }
1332
+ #check(name, received, args, origin) {
1333
+ const matcher = matchers[name];
1334
+ let result;
1335
+ try {
1336
+ result = matcher.call(this.#context, received, ...args);
1337
+ } catch (error) {
1338
+ throw _Assertion.#error(
1339
+ error instanceof Error ? error.message : String(error),
1340
+ origin
1341
+ );
1342
+ }
1343
+ if (result.pass === this.#context.isNot)
1344
+ throw _Assertion.#error(result.message(), origin);
1345
+ }
1346
+ static #error(message, origin) {
1347
+ const error = new AssertionError(message);
1348
+ const frames = origin.stack?.split("\n").slice(1).join("\n");
1349
+ error.stack = `${error.name}: ${message}${frames ? `
1350
+ ${frames}` : ""}`;
1351
+ return error;
1352
+ }
1353
+ };
1354
+ for (const [name, matcher] of Object.entries(matchers))
1355
+ Assertion.define(name, matcher);
1356
+
1357
+ // packages/testero/src/expect/asymmetric.ts
1358
+ var Asymmetric = class {
1359
+ sample;
1360
+ /** Set for the `expect.not` variants, which match what the rule does not. */
1361
+ inverse;
1362
+ constructor(sample, inverse = false) {
1363
+ this.sample = sample;
1364
+ this.inverse = inverse;
1365
+ }
1366
+ asymmetricMatch(other) {
1367
+ return this.matches(other) !== this.inverse;
1368
+ }
1369
+ get prefix() {
1370
+ return this.inverse ? "Not" : "";
1371
+ }
1372
+ };
1373
+ var Any = class extends Asymmetric {
1374
+ matches(other) {
1375
+ const primitives = /* @__PURE__ */ new Map([
1376
+ [String, "string"],
1377
+ [Number, "number"],
1378
+ [Boolean, "boolean"],
1379
+ [BigInt, "bigint"],
1380
+ [Symbol, "symbol"],
1381
+ [Function, "function"]
1382
+ ]);
1383
+ const type = primitives.get(this.sample);
1384
+ if (type && typeof other === type) return true;
1385
+ if (this.sample === Object)
1386
+ return typeof other === "object" && other !== null;
1387
+ return other instanceof this.sample;
1388
+ }
1389
+ toString() {
1390
+ return `Any<${this.sample.name}>`;
1391
+ }
1392
+ };
1393
+ var Anything = class extends Asymmetric {
1394
+ matches(other) {
1395
+ return other !== null && other !== void 0;
1396
+ }
1397
+ toString() {
1398
+ return "Anything";
1399
+ }
1400
+ };
1401
+ var ObjectContaining = class extends Asymmetric {
1402
+ matches(other) {
1403
+ if (typeof other !== "object" || other === null) return false;
1404
+ return Reflect.ownKeys(this.sample).every(
1405
+ (key) => key in other && Equality.equals(
1406
+ this.sample[key],
1407
+ other[key]
1408
+ )
1409
+ );
1410
+ }
1411
+ toString() {
1412
+ return `Object${this.prefix}Containing ${Format.inline(this.sample)}`;
1413
+ }
1414
+ };
1415
+ var ArrayContaining = class extends Asymmetric {
1416
+ matches(other) {
1417
+ return Array.isArray(other) && this.sample.every(
1418
+ (item) => other.some((value) => Equality.equals(item, value))
1419
+ );
1420
+ }
1421
+ toString() {
1422
+ return `Array${this.prefix}Containing ${Format.inline(this.sample)}`;
1423
+ }
1424
+ };
1425
+ var StringContaining = class extends Asymmetric {
1426
+ matches(other) {
1427
+ return typeof other === "string" && other.includes(this.sample);
1428
+ }
1429
+ toString() {
1430
+ return `String${this.prefix}Containing ${JSON.stringify(this.sample)}`;
1431
+ }
1432
+ };
1433
+ var StringMatching = class extends Asymmetric {
1434
+ matches(other) {
1435
+ return typeof other === "string" && this.sample.test(other);
1436
+ }
1437
+ toString() {
1438
+ return `String${this.prefix}Matching ${this.sample}`;
1439
+ }
1440
+ };
1441
+ var CloseTo = class extends Asymmetric {
1442
+ #digits;
1443
+ constructor(sample, digits = 2, inverse = false) {
1444
+ super(sample, inverse);
1445
+ this.#digits = digits;
1446
+ }
1447
+ matches(other) {
1448
+ if (typeof other !== "number") return false;
1449
+ if (other === this.sample) return true;
1450
+ return Math.abs(this.sample - other) < 10 ** -this.#digits / 2;
1451
+ }
1452
+ toString() {
1453
+ return `Number${this.prefix}CloseTo ${this.sample} (${this.#digits} digits)`;
1454
+ }
1455
+ };
1456
+
1457
+ // packages/testero/src/expect/expect.ts
1458
+ var negatable = (inverse) => ({
1459
+ objectContaining: (object) => new ObjectContaining(object, inverse),
1460
+ arrayContaining: (array) => new ArrayContaining(array, inverse),
1461
+ stringContaining: (text) => new StringContaining(text, inverse),
1462
+ stringMatching: (pattern) => new StringMatching(new RegExp(pattern), inverse),
1463
+ closeTo: (number, digits) => new CloseTo(number, digits, inverse)
1464
+ });
1465
+ var Expect = class {
1466
+ #expected;
1467
+ #needed = false;
1468
+ /** Starts the counts of a new test. */
1469
+ reset() {
1470
+ Assertion.count = 0;
1471
+ this.#expected = void 0;
1472
+ this.#needed = false;
1473
+ }
1474
+ /** Failures of `expect.assertions(n)` and `expect.hasAssertions()` after a test. */
1475
+ verify() {
1476
+ const count = Assertion.count;
1477
+ if (this.#expected !== void 0 && count !== this.#expected)
1478
+ return [
1479
+ new AssertionError(
1480
+ `expect.assertions(${this.#expected})
1481
+
1482
+ Expected ${this.#expected} assertion${this.#expected === 1 ? "" : "s"} to be called but received ${count} assertion call${count === 1 ? "" : "s"}.`
1483
+ )
1484
+ ];
1485
+ if (this.#needed && count === 0)
1486
+ return [
1487
+ new AssertionError(
1488
+ "expect.hasAssertions()\n\nExpected at least one assertion to be called but received none."
1489
+ )
1490
+ ];
1491
+ return [];
1492
+ }
1493
+ /** The function test files call as `expect`. */
1494
+ create() {
1495
+ const expect = (received) => new Assertion(received);
1496
+ return Object.assign(expect, {
1497
+ ...negatable(false),
1498
+ not: negatable(true),
1499
+ any: (type) => new Any(type),
1500
+ anything: () => new Anything(void 0),
1501
+ assertions: (count) => {
1502
+ this.#expected = count;
1503
+ },
1504
+ hasAssertions: () => {
1505
+ this.#needed = true;
1506
+ },
1507
+ extend: (added) => {
1508
+ for (const [name, matcher] of Object.entries(added))
1509
+ Assertion.define(name, matcher);
1510
+ }
1511
+ });
1512
+ }
1513
+ };
1514
+
1515
+ // packages/testero/src/globals.ts
1516
+ var Globals = class _Globals {
1517
+ static key = /* @__PURE__ */ Symbol.for("testero");
1518
+ static names = [
1519
+ "describe",
1520
+ "fdescribe",
1521
+ "xdescribe",
1522
+ "test",
1523
+ "it",
1524
+ "fit",
1525
+ "xit",
1526
+ "xtest",
1527
+ "beforeAll",
1528
+ "afterAll",
1529
+ "beforeEach",
1530
+ "afterEach",
1531
+ "expect",
1532
+ "testero",
1533
+ "jest"
1534
+ ];
1535
+ /** The module the transformer injects, it reads the globals of the file being run. */
1536
+ static source() {
1537
+ return [
1538
+ 'const globals = globalThis[Symbol.for("testero")];',
1539
+ ..._Globals.names.map(
1540
+ (name) => `export const ${name} = globals.${name};`
1541
+ )
1542
+ ].join("\n");
1543
+ }
1544
+ /** Sets the globals for the next test file to import. */
1545
+ static install(collector, expect, settings) {
1546
+ const { describe, test } = collector;
1547
+ const testero = {
1548
+ fn: Mocks.fn,
1549
+ spyOn: Mocks.spyOn,
1550
+ isMockFunction: Mocks.isMockFunction,
1551
+ clearAllMocks: () => {
1552
+ Mocks.clearAll();
1553
+ return testero;
1554
+ },
1555
+ resetAllMocks: () => {
1556
+ Mocks.resetAll();
1557
+ return testero;
1558
+ },
1559
+ restoreAllMocks: () => {
1560
+ Mocks.restoreAll();
1561
+ return testero;
1562
+ },
1563
+ /** Sets the timeout of the tests and hooks of this file. */
1564
+ setTimeout: (timeout) => {
1565
+ settings.timeout = timeout;
1566
+ return testero;
1567
+ }
1568
+ };
1569
+ globalThis[_Globals.key] = {
1570
+ describe,
1571
+ fdescribe: describe.only,
1572
+ xdescribe: describe.skip,
1573
+ test,
1574
+ it: test,
1575
+ fit: test.only,
1576
+ xit: test.skip,
1577
+ xtest: test.skip,
1578
+ beforeAll: collector.hook("beforeAll"),
1579
+ afterAll: collector.hook("afterAll"),
1580
+ beforeEach: collector.hook("beforeEach"),
1581
+ afterEach: collector.hook("afterEach"),
1582
+ expect: expect.create(),
1583
+ testero,
1584
+ jest: testero
1585
+ };
1586
+ }
1587
+ };
1588
+
1589
+ // packages/testero/src/reporter.ts
1590
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
1591
+ import { dirname as dirname2, isAbsolute, relative } from "node:path";
1592
+ import { fileURLToPath } from "node:url";
1593
+
1594
+ // packages/testero/src/transformer.ts
1595
+ import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
1596
+ import { tmpdir } from "node:os";
1597
+ import { dirname, extname, join as join2, sep } from "node:path";
1598
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
1599
+ import { build } from "esbuild";
1600
+ var Transformer = class _Transformer {
1601
+ /** Packages test files import the globals from, like with Jest. */
1602
+ static modules = ["@jest/globals", "@aljazmc/testero"];
1603
+ static #loaders = {
1604
+ ".js": "js",
1605
+ ".cjs": "js",
1606
+ ".mjs": "js",
1607
+ ".jsx": "jsx",
1608
+ ".ts": "ts",
1609
+ ".cts": "ts",
1610
+ ".mts": "ts",
1611
+ ".tsx": "tsx"
1612
+ };
1613
+ /** Marks the resolutions the plugin asks esbuild for itself. */
1614
+ static #own = /* @__PURE__ */ Symbol("testero");
1615
+ /** Where compiled test files go, stack frames there are testero's own. */
1616
+ static prefix = join2(tmpdir(), "testero-build-");
1617
+ #directory = mkdtempSync(_Transformer.prefix);
1618
+ #globals = join2(this.#directory, "globals.mjs");
1619
+ #count = 0;
6
1620
  constructor() {
7
- this.greeting = "Hello, test runner!";
1621
+ writeFileSync(this.#globals, Globals.source());
1622
+ }
1623
+ /** Bundles a test file, returns the URL of the module to import. */
1624
+ async compile(file) {
1625
+ const outfile = join2(this.#directory, `${++this.#count}.mjs`);
1626
+ const { outputFiles } = await build({
1627
+ entryPoints: [file],
1628
+ outfile,
1629
+ write: false,
1630
+ bundle: true,
1631
+ platform: "node",
1632
+ format: "esm",
1633
+ target: `node${process.versions.node.split(".")[0]}`,
1634
+ sourcemap: "inline",
1635
+ sourcesContent: false,
1636
+ inject: [this.#globals],
1637
+ // CommonJS code in the bundle may require installed packages
1638
+ banner: {
1639
+ js: 'import { createRequire as __testeroRequire } from "node:module";\nconst require = __testeroRequire(import.meta.url);'
1640
+ },
1641
+ logLevel: "silent",
1642
+ plugins: [this.#plugin()]
1643
+ });
1644
+ writeFileSync(outfile, outputFiles[0]?.text ?? "");
1645
+ return pathToFileURL2(outfile).href;
1646
+ }
1647
+ /** Removes the compiled files. */
1648
+ dispose() {
1649
+ rmSync(this.#directory, { recursive: true, force: true });
8
1650
  }
9
- }();
1651
+ #plugin() {
1652
+ return {
1653
+ name: "testero",
1654
+ setup: (build2) => {
1655
+ build2.onResolve({ filter: /^[^./]/ }, async (args) => {
1656
+ if (args.pluginData === _Transformer.#own) return void 0;
1657
+ if (_Transformer.modules.includes(args.path))
1658
+ return { path: this.#globals };
1659
+ const resolved = await build2.resolve(args.path, {
1660
+ kind: args.kind,
1661
+ resolveDir: args.resolveDir,
1662
+ pluginData: _Transformer.#own
1663
+ });
1664
+ if (resolved.errors.length > 0)
1665
+ return { errors: resolved.errors };
1666
+ if (resolved.external)
1667
+ return { path: args.path, external: true };
1668
+ if (!_Transformer.#installed(resolved.path))
1669
+ return { path: resolved.path };
1670
+ return {
1671
+ path: args.kind === "require-call" ? resolved.path : pathToFileURL2(resolved.path).href,
1672
+ external: true
1673
+ };
1674
+ });
1675
+ build2.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
1676
+ if (_Transformer.#installed(args.path)) return void 0;
1677
+ const values = {
1678
+ url: pathToFileURL2(args.path).href,
1679
+ filename: args.path,
1680
+ dirname: dirname(args.path)
1681
+ };
1682
+ const contents = readFileSync2(args.path, "utf8").replace(
1683
+ /\bimport\.meta\.(url|filename|dirname)\b/g,
1684
+ (_, name) => JSON.stringify(values[name])
1685
+ );
1686
+ return {
1687
+ contents,
1688
+ loader: _Transformer.#loaders[extname(args.path)] ?? "js",
1689
+ resolveDir: dirname(args.path)
1690
+ };
1691
+ });
1692
+ }
1693
+ };
1694
+ }
1695
+ static #installed(path) {
1696
+ return path.split(sep).includes("node_modules");
1697
+ }
1698
+ };
1699
+
1700
+ // packages/testero/src/reporter.ts
1701
+ var Reporter = class _Reporter {
1702
+ static #codes = {
1703
+ bold: 1,
1704
+ dim: 2,
1705
+ inverse: 7,
1706
+ red: 31,
1707
+ green: 32,
1708
+ yellow: 33,
1709
+ magenta: 35,
1710
+ gray: 90
1711
+ };
1712
+ /** Stack frames from testero itself are left out of failures. */
1713
+ static #own = dirname2(fileURLToPath(import.meta.url));
1714
+ #root;
1715
+ #verbose;
1716
+ #color;
1717
+ #write;
1718
+ constructor(root, options = {}) {
1719
+ this.#root = root;
1720
+ this.#verbose = options.verbose ?? false;
1721
+ this.#color = options.color ?? (process.stdout.isTTY === true && !process.env.NO_COLOR);
1722
+ this.#write = options.write ?? ((text) => process.stdout.write(text));
1723
+ }
1724
+ static failed(result) {
1725
+ return result.errors.length > 0 || result.results.some((test) => test.status === "failed");
1726
+ }
1727
+ /** The results of a test file, with the failures in detail. */
1728
+ file(result) {
1729
+ const failed = _Reporter.failed(result);
1730
+ const status = failed ? this.#paint(" FAIL ", "inverse", "bold", "red") : this.#paint(" PASS ", "inverse", "bold", "green");
1731
+ const slow = result.duration > 1e3 ? ` (${(result.duration / 1e3).toFixed(2)} s)` : "";
1732
+ const lines = [`${status} ${result.file}${slow}`];
1733
+ if (this.#verbose || failed) lines.push(...this.#tree(result.results));
1734
+ for (const error of result.errors)
1735
+ lines.push(
1736
+ "",
1737
+ this.#paint(" \u25CF Test suite failed to run", "bold", "red"),
1738
+ "",
1739
+ this.#error(error)
1740
+ );
1741
+ for (const test of result.results)
1742
+ for (const error of test.errors)
1743
+ lines.push(
1744
+ "",
1745
+ this.#paint(
1746
+ ` \u25CF ${test.test.titles.join(" \u203A ")}`,
1747
+ "bold",
1748
+ "red"
1749
+ ),
1750
+ "",
1751
+ this.#error(error)
1752
+ );
1753
+ this.#write(`${lines.join("\n")}
1754
+ ${failed ? "\n" : ""}`);
1755
+ }
1756
+ /** Counts of test files and tests, and the time they took. */
1757
+ summary(files, duration) {
1758
+ const results = files.flatMap((file) => file.results);
1759
+ const count = (status) => results.filter((result) => result.status === status).length;
1760
+ const failedFiles = files.filter(_Reporter.failed).length;
1761
+ const parts = (counts, total) => [
1762
+ ...counts.filter(([amount]) => amount > 0).map(
1763
+ ([amount, label, color]) => this.#paint(`${amount} ${label}`, "bold", color)
1764
+ ),
1765
+ `${total} total`
1766
+ ].join(", ");
1767
+ this.#write(
1768
+ [
1769
+ "",
1770
+ `${this.#paint("Test Suites:", "bold")} ${parts(
1771
+ [
1772
+ [failedFiles, "failed", "red"],
1773
+ [files.length - failedFiles, "passed", "green"]
1774
+ ],
1775
+ files.length
1776
+ )}`,
1777
+ `${this.#paint("Tests:", "bold")} ${parts(
1778
+ [
1779
+ [count("failed"), "failed", "red"],
1780
+ [count("skipped"), "skipped", "yellow"],
1781
+ [count("todo"), "todo", "magenta"],
1782
+ [count("passed"), "passed", "green"]
1783
+ ],
1784
+ results.length
1785
+ )}`,
1786
+ `${this.#paint("Time:", "bold")} ${(duration / 1e3).toFixed(3)} s`,
1787
+ ""
1788
+ ].join("\n")
1789
+ );
1790
+ }
1791
+ /** Explains that no test files were found. */
1792
+ none(patterns) {
1793
+ this.#write(
1794
+ `No tests found${patterns.length > 0 ? `, no files match ${patterns.map((pattern) => JSON.stringify(pattern)).join(", ")}` : ""}.
1795
+ `
1796
+ );
1797
+ }
1798
+ /** Tests indented under their suites, with a mark for how each went. */
1799
+ #tree(results) {
1800
+ const lines = [];
1801
+ let previous = [];
1802
+ for (const { test, status, duration } of results) {
1803
+ const titles = test.suite.titles;
1804
+ let shared = 0;
1805
+ while (shared < titles.length && titles[shared] === previous[shared])
1806
+ shared++;
1807
+ for (const [depth, title] of titles.slice(shared).entries())
1808
+ lines.push(`${" ".repeat(shared + depth + 1)}${title}`);
1809
+ previous = titles;
1810
+ const marks = {
1811
+ passed: this.#paint("\u2713", "green"),
1812
+ failed: this.#paint("\u2715", "red"),
1813
+ skipped: this.#paint("\u25CB skipped", "yellow"),
1814
+ todo: this.#paint("\u270E todo", "magenta")
1815
+ };
1816
+ const time = duration >= 1 ? this.#paint(` (${Math.round(duration)} ms)`, "gray") : "";
1817
+ lines.push(
1818
+ `${" ".repeat(titles.length + 1)}${marks[status]} ${test.name}${status === "passed" || status === "failed" ? time : ""}`
1819
+ );
1820
+ }
1821
+ return lines;
1822
+ }
1823
+ /** The message of an error, the code where it happened and its stack. */
1824
+ #error(error) {
1825
+ const message = error instanceof AssertionError ? error.message : error instanceof Error ? `${error.name}: ${error.message}` : `thrown: ${Format.inline(error)}`;
1826
+ const frames = error instanceof Error ? this.#frames(error.stack ?? "") : [];
1827
+ const [first] = frames;
1828
+ const sections = [this.#highlight(message)];
1829
+ const code = first ? this.#code(first) : "";
1830
+ if (code) sections.push(code);
1831
+ if (frames.length > 0)
1832
+ sections.push(
1833
+ frames.map(
1834
+ (frame) => this.#paint(
1835
+ `at ${this.#path(frame.file)}:${frame.line}:${frame.column}`,
1836
+ "gray"
1837
+ )
1838
+ ).join("\n")
1839
+ );
1840
+ return _Reporter.#indent(sections.join("\n\n"), " ");
1841
+ }
1842
+ /** Colors expected values green and received values red, like Jest. */
1843
+ #highlight(message) {
1844
+ return message.split("\n").map((line) => {
1845
+ if (/^(?:Expected|- )/.test(line))
1846
+ return this.#paint(line, "green");
1847
+ if (/^(?:Received|\+ )/.test(line))
1848
+ return this.#paint(line, "red");
1849
+ return line;
1850
+ }).join("\n");
1851
+ }
1852
+ /** Stack frames of the code under test, without Node's and testero's own. */
1853
+ #frames(stack) {
1854
+ const frames = [];
1855
+ for (const line of stack.split("\n")) {
1856
+ const match = /^\s+at (?:.*?\()?(.+?):(\d+):(\d+)\)?$/.exec(line);
1857
+ if (!match) continue;
1858
+ let file = match[1];
1859
+ if (file.startsWith("file://")) file = fileURLToPath(file);
1860
+ if (!isAbsolute(file) || file.includes("node_modules") || file.startsWith(_Reporter.#own) || file.startsWith(Transformer.prefix))
1861
+ continue;
1862
+ frames.push({
1863
+ file,
1864
+ line: Number(match[2]),
1865
+ column: Number(match[3])
1866
+ });
1867
+ }
1868
+ return frames;
1869
+ }
1870
+ /** The lines around a position, with a mark under it. */
1871
+ #code({ file, line, column }) {
1872
+ if (!existsSync2(file)) return "";
1873
+ const lines = readFileSync3(file, "utf8").split(/\r?\n/);
1874
+ const first = Math.max(1, line - 2);
1875
+ const last = Math.min(lines.length, line + 2);
1876
+ const width = String(last).length;
1877
+ const out = [];
1878
+ for (let number = first; number <= last; number++) {
1879
+ const gutter = `${String(number).padStart(width)} |`;
1880
+ const text = lines[number - 1] ?? "";
1881
+ if (number === line) {
1882
+ out.push(
1883
+ `${this.#paint(">", "red", "bold")} ${gutter} ${text}`
1884
+ );
1885
+ out.push(
1886
+ ` ${" ".repeat(width)} | ${" ".repeat(column - 1)}${this.#paint("^", "red", "bold")}`
1887
+ );
1888
+ } else out.push(this.#paint(` ${gutter} ${text}`, "dim"));
1889
+ }
1890
+ return out.join("\n");
1891
+ }
1892
+ #path(file) {
1893
+ const path = relative(this.#root, file);
1894
+ return path.startsWith("..") ? file : path;
1895
+ }
1896
+ #paint(text, ...styles) {
1897
+ if (!this.#color) return text;
1898
+ const codes = styles.map((style) => _Reporter.#codes[style]).join(";");
1899
+ return `\x1B[${codes}m${text}\x1B[0m`;
1900
+ }
1901
+ static #indent(text, indent) {
1902
+ return text.split("\n").map((line) => line ? indent + line : line).join("\n");
1903
+ }
1904
+ };
1905
+
1906
+ // packages/testero/src/runner.ts
1907
+ var Runner = class _Runner {
1908
+ #settings;
1909
+ #focused = false;
1910
+ constructor(settings) {
1911
+ this.#settings = settings;
1912
+ }
1913
+ /** Results of every test, and errors of `afterAll` hooks that belong to no test. */
1914
+ async run(root) {
1915
+ this.#focused = [...root.tests()].some((test) => test.focused);
1916
+ const results = [];
1917
+ const errors = [];
1918
+ await this.#suite(root, [], results, errors);
1919
+ return { results, errors };
1920
+ }
1921
+ /** Whether a test runs: not skipped, focused when others are, matching the name pattern. */
1922
+ runs(test) {
1923
+ return !test.skipped && (!this.#focused || test.focused) && (this.#settings.namePattern?.test(test.fullName) ?? true);
1924
+ }
1925
+ async #suite(suite, failed, results, errors) {
1926
+ if (![...suite.tests()].some((test) => this.runs(test))) {
1927
+ for (const test of suite.tests()) results.push(_Runner.#skip(test));
1928
+ return;
1929
+ }
1930
+ const setup = [...failed];
1931
+ if (setup.length === 0)
1932
+ for (const hook of suite.hooks.beforeAll) {
1933
+ const error = await this.#call(hook.body, hook.timeout);
1934
+ if (error !== void 0) {
1935
+ setup.push(error);
1936
+ break;
1937
+ }
1938
+ }
1939
+ for (const child of suite.children)
1940
+ if (child instanceof Suite)
1941
+ await this.#suite(child, setup, results, errors);
1942
+ else await this.#test(child, setup, results);
1943
+ if (failed.length === 0)
1944
+ for (const hook of suite.hooks.afterAll) {
1945
+ const error = await this.#call(hook.body, hook.timeout);
1946
+ if (error !== void 0) errors.push(error);
1947
+ }
1948
+ }
1949
+ async #test(test, failed, results) {
1950
+ if (!this.runs(test)) {
1951
+ results.push(_Runner.#skip(test));
1952
+ return;
1953
+ }
1954
+ const start = performance.now();
1955
+ const errors = [...failed];
1956
+ if (errors.length === 0) {
1957
+ this.#settings.beforeTest?.(test);
1958
+ for (const hook of _Runner.#hooks(test, "beforeEach")) {
1959
+ const error = await this.#call(hook.body, hook.timeout);
1960
+ if (error !== void 0) {
1961
+ errors.push(error);
1962
+ break;
1963
+ }
1964
+ }
1965
+ if (errors.length === 0 && test.body) {
1966
+ const error = await this.#call(test.body, test.timeout);
1967
+ if (error !== void 0) errors.push(error);
1968
+ }
1969
+ for (const hook of _Runner.#hooks(test, "afterEach").reverse()) {
1970
+ const error = await this.#call(hook.body, hook.timeout);
1971
+ if (error !== void 0) errors.push(error);
1972
+ }
1973
+ errors.push(...this.#settings.afterTest?.(test) ?? []);
1974
+ }
1975
+ results.push({
1976
+ test,
1977
+ status: errors.length > 0 ? "failed" : "passed",
1978
+ duration: performance.now() - start,
1979
+ errors
1980
+ });
1981
+ }
1982
+ /** Hooks of a kind from the outermost suite to the test's own. */
1983
+ static #hooks(test, kind) {
1984
+ return test.suite.path.flatMap((suite) => suite.hooks[kind]);
1985
+ }
1986
+ static #skip(test) {
1987
+ return {
1988
+ test,
1989
+ status: test.mode === "todo" ? "todo" : "skipped",
1990
+ duration: 0,
1991
+ errors: []
1992
+ };
1993
+ }
1994
+ /** Calls a body within its timeout, returning what it threw, or undefined. */
1995
+ async #call(body, timeout = this.#settings.timeout) {
1996
+ let timer;
1997
+ const expired = new Promise((_, reject) => {
1998
+ timer = setTimeout(
1999
+ () => reject(
2000
+ new Error(
2001
+ `Exceeded timeout of ${timeout} ms for a test or hook.`
2002
+ )
2003
+ ),
2004
+ timeout
2005
+ );
2006
+ });
2007
+ try {
2008
+ await Promise.race([_Runner.#invoke(body), expired]);
2009
+ return void 0;
2010
+ } catch (error) {
2011
+ return error ?? new Error(`${String(error)} was thrown`);
2012
+ } finally {
2013
+ clearTimeout(timer);
2014
+ }
2015
+ }
2016
+ /** Runs a body as a promise, with a `done` callback when it takes one. */
2017
+ static #invoke(body) {
2018
+ if (body.length === 0)
2019
+ return new Promise((resolve) => resolve(body(() => {
2020
+ })));
2021
+ return new Promise((resolve, reject) => {
2022
+ body(
2023
+ (error) => error === void 0 ? resolve(void 0) : reject(error)
2024
+ );
2025
+ });
2026
+ }
2027
+ };
2028
+
2029
+ // packages/testero/src/testero.ts
2030
+ var Testero = class {
2031
+ #config;
2032
+ #options;
2033
+ #reporter;
2034
+ /** Errors thrown outside of tests, like from a forgotten timer. */
2035
+ #stray = [];
2036
+ constructor(config, options, reporter = new Reporter(config.root, { verbose: options.verbose })) {
2037
+ this.#config = config;
2038
+ this.#options = options;
2039
+ this.#reporter = reporter;
2040
+ }
2041
+ /** The test files to run, relative to the root. */
2042
+ files() {
2043
+ const includes = new Includes(this.#options.testMatch);
2044
+ const patterns = this.#options.patterns.map(
2045
+ (pattern) => new RegExp(pattern)
2046
+ );
2047
+ return [...this.#config.files()].filter(
2048
+ (file) => includes.matches(file) && (patterns.length === 0 || patterns.some((pattern) => pattern.test(file)))
2049
+ ).sort();
2050
+ }
2051
+ /** Runs every test file, returns whether all of them passed. */
2052
+ async run() {
2053
+ const start = performance.now();
2054
+ const files = this.files();
2055
+ if (files.length === 0) {
2056
+ this.#reporter.none(this.#options.patterns);
2057
+ return this.#options.passWithNoTests;
2058
+ }
2059
+ process.setSourceMapsEnabled(true);
2060
+ const stray = (error) => this.#stray.push(error);
2061
+ process.on("uncaughtException", stray);
2062
+ process.on("unhandledRejection", stray);
2063
+ const transformer = new Transformer();
2064
+ const results = [];
2065
+ try {
2066
+ for (const file of files) {
2067
+ const result = await this.#file(file, transformer);
2068
+ results.push(result);
2069
+ this.#reporter.file(result);
2070
+ if (this.#options.bail && Reporter.failed(result)) break;
2071
+ }
2072
+ } finally {
2073
+ transformer.dispose();
2074
+ process.off("uncaughtException", stray);
2075
+ process.off("unhandledRejection", stray);
2076
+ }
2077
+ this.#reporter.summary(results, performance.now() - start);
2078
+ return !results.some(Reporter.failed);
2079
+ }
2080
+ async #file(file, transformer) {
2081
+ const start = performance.now();
2082
+ const finish = (results2, errors2) => {
2083
+ errors2.push(...this.#stray.splice(0));
2084
+ Mocks.restoreAll();
2085
+ Mocks.forget();
2086
+ return {
2087
+ file,
2088
+ results: results2,
2089
+ errors: errors2,
2090
+ duration: performance.now() - start
2091
+ };
2092
+ };
2093
+ const collector = new Collector();
2094
+ const expect = new Expect();
2095
+ const settings = {
2096
+ timeout: this.#options.testTimeout,
2097
+ namePattern: this.#options.namePattern,
2098
+ beforeTest: () => {
2099
+ expect.reset();
2100
+ if (this.#options.restoreMocks) Mocks.restoreAll();
2101
+ else if (this.#options.resetMocks) Mocks.resetAll();
2102
+ else if (this.#options.clearMocks) Mocks.clearAll();
2103
+ },
2104
+ afterTest: () => expect.verify()
2105
+ };
2106
+ Globals.install(collector, expect, settings);
2107
+ try {
2108
+ await import(await transformer.compile(join3(this.#config.root, file)));
2109
+ } catch (error) {
2110
+ return finish([], [error]);
2111
+ } finally {
2112
+ collector.close();
2113
+ }
2114
+ if ([...collector.root.tests()].length === 0)
2115
+ return finish(
2116
+ [],
2117
+ [new Error("Your test suite must contain at least one test.")]
2118
+ );
2119
+ const { results, errors } = await new Runner(settings).run(
2120
+ collector.root
2121
+ );
2122
+ return finish(results, errors);
2123
+ }
2124
+ };
10
2125
 
11
2126
  // packages/testero/src/index.ts
12
- console.log(testero_default.greeting);
2127
+ try {
2128
+ const args = new Arguments(process.argv.slice(2));
2129
+ if (args.help) process.stdout.write(Arguments.usage);
2130
+ else {
2131
+ const config = await Config.load("testero", config_default);
2132
+ const testero = new Testero(config, args.options(config.rules));
2133
+ if (args.listTests)
2134
+ for (const file of testero.files()) console.log(file);
2135
+ else if (!await testero.run()) process.exitCode = 1;
2136
+ }
2137
+ } catch (error) {
2138
+ console.error(error instanceof Error ? error.message : error);
2139
+ process.exitCode = 1;
2140
+ }