@unrift/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/unrift.mjs +2 -1
- package/dist/esm/cli.js +19 -1
- package/dist/esm/expect.d.ts +14 -0
- package/dist/esm/expect.d.ts.map +1 -1
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/interface.d.ts +7 -3
- package/dist/esm/interface.d.ts.map +1 -1
- package/dist/esm/interface.js +9 -5
- package/dist/esm/matchers.d.ts.map +1 -1
- package/dist/esm/matchers.js +195 -1
- package/dist/esm/mock/auto.d.ts +27 -0
- package/dist/esm/mock/auto.d.ts.map +1 -0
- package/dist/esm/mock/auto.js +108 -0
- package/dist/esm/mock/fetch.d.ts +47 -0
- package/dist/esm/mock/fetch.d.ts.map +1 -0
- package/dist/esm/mock/fetch.js +149 -0
- package/dist/esm/mock/fs.d.ts +54 -0
- package/dist/esm/mock/fs.d.ts.map +1 -0
- package/dist/esm/mock/fs.js +125 -0
- package/dist/esm/mock/index.d.ts +70 -0
- package/dist/esm/mock/index.d.ts.map +1 -0
- package/dist/esm/mock/index.js +70 -0
- package/dist/esm/mock/module-loader.d.ts +66 -0
- package/dist/esm/mock/module-loader.d.ts.map +1 -0
- package/dist/esm/mock/module-loader.js +123 -0
- package/dist/esm/mock/module.d.ts +99 -0
- package/dist/esm/mock/module.d.ts.map +1 -0
- package/dist/esm/mock/module.js +198 -0
- package/dist/esm/mock/register-loader.d.ts +17 -0
- package/dist/esm/mock/register-loader.d.ts.map +1 -0
- package/dist/esm/mock/register-loader.js +45 -0
- package/dist/esm/mock/spy.d.ts +73 -0
- package/dist/esm/mock/spy.d.ts.map +1 -0
- package/dist/esm/mock/spy.js +307 -0
- package/dist/esm/mock/stub.d.ts +26 -0
- package/dist/esm/mock/stub.d.ts.map +1 -0
- package/dist/esm/mock/stub.js +82 -0
- package/dist/esm/mock/timers.d.ts +51 -0
- package/dist/esm/mock/timers.d.ts.map +1 -0
- package/dist/esm/mock/timers.js +581 -0
- package/dist/esm/run.d.ts +4 -7
- package/dist/esm/run.d.ts.map +1 -1
- package/dist/esm/run.js +6 -2
- package/dist/esm/runner.d.ts +1 -0
- package/dist/esm/runner.d.ts.map +1 -1
- package/dist/esm/runner.js +21 -10
- package/dist/esm/suite.d.ts +24 -6
- package/dist/esm/suite.d.ts.map +1 -1
- package/dist/esm/suite.js +114 -20
- package/dist/esm/test.d.ts +5 -1
- package/dist/esm/test.d.ts.map +1 -1
- package/dist/esm/test.js +20 -6
- package/dist/esm/utils/compareEngine.js +3 -3
- package/dist/esm/utils/defineConfig.d.ts +1 -0
- package/dist/esm/utils/defineConfig.d.ts.map +1 -1
- package/dist/esm/utils/discoverTestFiles.js +1 -1
- package/dist/esm/utils/helpers.d.ts +12 -0
- package/dist/esm/utils/helpers.d.ts.map +1 -1
- package/dist/esm/utils/helpers.js +20 -0
- package/dist/esm/utils/toThrow.d.ts.map +1 -1
- package/dist/esm/utils/toThrow.js +2 -1
- package/package.json +4 -3
package/dist/esm/runner.js
CHANGED
|
@@ -8,6 +8,11 @@ import { runEngine } from "./run.js";
|
|
|
8
8
|
import { normalizePath } from "./utils/normalizePath.js";
|
|
9
9
|
import { safeRegExp, isGlobPattern, globToRegex } from "./utils/helpers.js";
|
|
10
10
|
import { TaskStatus } from "./types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Captured at module load so the runner's own timing measurements
|
|
13
|
+
* aren't affected by user mocks (e.g. mock.useFakeTimers patches performance.now).
|
|
14
|
+
*/
|
|
15
|
+
const realPerformanceNow = performance.now.bind(performance);
|
|
11
16
|
function debugLog(enabled, json, ...args) {
|
|
12
17
|
// Keep stdout clean for --json mode; use stderr for debug.
|
|
13
18
|
if (!enabled)
|
|
@@ -144,7 +149,7 @@ function extractTestName(description) {
|
|
|
144
149
|
}
|
|
145
150
|
export async function runTestsCLI(options = {}) {
|
|
146
151
|
regExpCache.clear();
|
|
147
|
-
const runStart =
|
|
152
|
+
const runStart = realPerformanceNow();
|
|
148
153
|
// Cache clean mode
|
|
149
154
|
if (options.cacheClean) {
|
|
150
155
|
const projectRoot = process.cwd();
|
|
@@ -159,7 +164,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
159
164
|
if (result.tempCacheDeleted)
|
|
160
165
|
lines.push("✔ deleted temp cache");
|
|
161
166
|
if (lines.length === 0)
|
|
162
|
-
lines.push("
|
|
167
|
+
lines.push("- nothing to clean");
|
|
163
168
|
console.log(lines.join("\n"));
|
|
164
169
|
}
|
|
165
170
|
return;
|
|
@@ -215,7 +220,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
215
220
|
testDir: dir,
|
|
216
221
|
configPath: resolvedConfigPath,
|
|
217
222
|
files: [],
|
|
218
|
-
durationMs:
|
|
223
|
+
durationMs: realPerformanceNow() - runStart,
|
|
219
224
|
results: [],
|
|
220
225
|
};
|
|
221
226
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -232,6 +237,8 @@ export async function runTestsCLI(options = {}) {
|
|
|
232
237
|
files,
|
|
233
238
|
timeoutMs,
|
|
234
239
|
bail,
|
|
240
|
+
grep: options.grep,
|
|
241
|
+
retries: config?.retries,
|
|
235
242
|
matchers: config?.matchers,
|
|
236
243
|
});
|
|
237
244
|
debugLog(options.debug, options.json, "Only mode:", engine.isOnly);
|
|
@@ -259,13 +266,14 @@ export async function runTestsCLI(options = {}) {
|
|
|
259
266
|
description: result.description,
|
|
260
267
|
status: result.status,
|
|
261
268
|
durationMs: result.durationMs ?? 0,
|
|
269
|
+
attempts: result.attempts && result.attempts > 1 ? result.attempts : undefined,
|
|
262
270
|
error: result.error
|
|
263
271
|
? { message: result.error.message, stack: result.error.stack }
|
|
264
272
|
: undefined,
|
|
265
273
|
});
|
|
266
274
|
}
|
|
267
275
|
const ok = failed === 0;
|
|
268
|
-
const durationMs =
|
|
276
|
+
const durationMs = realPerformanceNow() - runStart;
|
|
269
277
|
// JSON mode: stdout must be JSON only
|
|
270
278
|
if (options.json) {
|
|
271
279
|
const report = {
|
|
@@ -288,7 +296,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
288
296
|
process.exitCode = 1;
|
|
289
297
|
return;
|
|
290
298
|
}
|
|
291
|
-
//
|
|
299
|
+
// Pretty reporting
|
|
292
300
|
const groups = groupResultsByFile(results);
|
|
293
301
|
// File-by-file results
|
|
294
302
|
for (const group of groups) {
|
|
@@ -323,14 +331,17 @@ export async function runTestsCLI(options = {}) {
|
|
|
323
331
|
lastSuitePath = suitePath;
|
|
324
332
|
}
|
|
325
333
|
const indent = " " + " ".repeat(depth);
|
|
334
|
+
const retried = result.attempts && result.attempts > 1
|
|
335
|
+
? colors.yellow(` (${result.status === TaskStatus.Fail ? "failed" : "passed"} after ${result.attempts} attempts)`)
|
|
336
|
+
: "";
|
|
326
337
|
if (result.status === TaskStatus.Pass) {
|
|
327
338
|
const dur = colors.dim(formatDuration(result.durationMs ?? 0));
|
|
328
|
-
console.log(`${indent}${colors.green("✔")} ${colors.dim(leaf)} ${dur}`);
|
|
339
|
+
console.log(`${indent}${colors.green("✔")} ${colors.dim(leaf)}${retried} ${dur}`);
|
|
329
340
|
continue;
|
|
330
341
|
}
|
|
331
342
|
if (result.status === TaskStatus.Fail) {
|
|
332
343
|
const dur = colors.dim(formatDuration(result.durationMs ?? 0));
|
|
333
|
-
console.log(`${indent}${colors.red("✘")} ${colors.boldRed(leaf)} ${dur}`);
|
|
344
|
+
console.log(`${indent}${colors.red("✘")} ${colors.boldRed(leaf)}${retried} ${dur}`);
|
|
334
345
|
continue;
|
|
335
346
|
}
|
|
336
347
|
if (result.status === TaskStatus.Skipped) {
|
|
@@ -343,7 +354,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
343
354
|
}
|
|
344
355
|
}
|
|
345
356
|
}
|
|
346
|
-
//
|
|
357
|
+
// Failure details
|
|
347
358
|
if (failures.length > 0) {
|
|
348
359
|
console.log(`\n${horizontalRule(colors.boldRed(" FAILURES "))}\n`);
|
|
349
360
|
failures.forEach((r, i) => {
|
|
@@ -373,7 +384,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
373
384
|
});
|
|
374
385
|
console.log(`\n${horizontalRule()}`);
|
|
375
386
|
}
|
|
376
|
-
//
|
|
387
|
+
// Summary
|
|
377
388
|
console.log("");
|
|
378
389
|
const summaryParts = [];
|
|
379
390
|
if (passed > 0)
|
|
@@ -461,7 +472,7 @@ export async function watchTestsCLI(options = {}) {
|
|
|
461
472
|
running = true;
|
|
462
473
|
const shortFile = shortenPath(filename);
|
|
463
474
|
console.log(`\n${colors.dim("─".repeat(Math.min(process.stdout.columns || 80, 80)))}`);
|
|
464
|
-
console.log(` ${colors.dim("↺")} ${colors.dim(`${shortFile} changed
|
|
475
|
+
console.log(` ${colors.dim("↺")} ${colors.dim(`${shortFile} changed - re-running…`)}\n`);
|
|
465
476
|
// Reset exit code so each run is evaluated independently
|
|
466
477
|
process.exitCode = 0;
|
|
467
478
|
try {
|
package/dist/esm/suite.d.ts
CHANGED
|
@@ -3,10 +3,20 @@ import { type PromisableFn, TaskMode, TaskStatus } from "./types";
|
|
|
3
3
|
interface RunOptions {
|
|
4
4
|
timeoutMs?: number;
|
|
5
5
|
bail?: boolean;
|
|
6
|
+
grep?: RegExp;
|
|
7
|
+
retries?: number;
|
|
6
8
|
}
|
|
7
9
|
interface SuiteRunState {
|
|
8
10
|
bailed: boolean;
|
|
9
11
|
}
|
|
12
|
+
export interface TestResult {
|
|
13
|
+
description: string;
|
|
14
|
+
status: TaskStatus;
|
|
15
|
+
error?: Error;
|
|
16
|
+
durationMs: number;
|
|
17
|
+
/** How many times the test ran. Above 1 means it was retried. */
|
|
18
|
+
attempts?: number;
|
|
19
|
+
}
|
|
10
20
|
export declare class Suite {
|
|
11
21
|
description: string;
|
|
12
22
|
mode: TaskMode;
|
|
@@ -27,20 +37,28 @@ export declare class Suite {
|
|
|
27
37
|
afterEachHooks: PromisableFn<void>[];
|
|
28
38
|
constructor(description: string, parent?: Suite, mode?: TaskMode);
|
|
29
39
|
markSubtreeSkipped(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Used when --grep prunes a suite. Matching todos stay todo so their status
|
|
42
|
+
* doesn't depend on whether a runnable sibling matched; everything else is
|
|
43
|
+
* skipped.
|
|
44
|
+
*/
|
|
45
|
+
private markSubtreeFilteredOut;
|
|
30
46
|
markSubtreeTodo(): void;
|
|
31
47
|
addSuite(suite: Suite): void;
|
|
32
48
|
addTest(test: Test): void;
|
|
33
49
|
private collectBeforeEachHooks;
|
|
34
50
|
private collectAfterEachHooks;
|
|
35
51
|
run(options?: RunOptions, state?: SuiteRunState, isOnly?: boolean, ancestorOnly?: boolean): Promise<void>;
|
|
52
|
+
private testMatchesGrep;
|
|
53
|
+
/**
|
|
54
|
+
* True if a test below this suite matches grep AND would actually run, using
|
|
55
|
+
* the same skip/todo/only rules as run(). A matching test that won't run
|
|
56
|
+
* shouldn't cause this suite's hooks to fire.
|
|
57
|
+
*/
|
|
58
|
+
private hasRunnableMatch;
|
|
36
59
|
getFullDescription(): string;
|
|
37
60
|
reset(): void;
|
|
38
|
-
getResults():
|
|
39
|
-
description: string;
|
|
40
|
-
status: TaskStatus;
|
|
41
|
-
error?: Error;
|
|
42
|
-
durationMs: number;
|
|
43
|
-
}>;
|
|
61
|
+
getResults(): TestResult[];
|
|
44
62
|
}
|
|
45
63
|
export declare function computeOnlyFlags(suite: Suite): boolean;
|
|
46
64
|
export declare const rootSuite: Suite;
|
package/dist/esm/suite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suite.d.ts","sourceRoot":"","sources":["../../src/suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,OAAO,EAAE,KAAK,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"suite.d.ts","sourceRoot":"","sources":["../../src/suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,OAAO,EAAE,KAAK,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAIlE,UAAU,UAAU;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,OAAO,CAAC;CACjB;AAsBD,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,KAAK;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,CAAC,EAAE,KAAK,CAAC;IAEf,cAAc,EAAE,OAAO,CAAS;IAEhC,IAAI,EAAE,OAAO,CAAS;IACtB,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAM;IACpD,MAAM,EAAE,KAAK,EAAE,CAAM;IACrB,KAAK,EAAE,IAAI,EAAE,CAAM;IAEnB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,cAAc,CAAC,EAAE,KAAK,CAAC;IAEvB,cAAc,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAM;IAC1C,aAAa,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAM;IACzC,eAAe,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAM;IAC3C,cAAc,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAM;gBAGxC,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,KAAK,EACd,IAAI,GAAE,QAA2B;IAO5B,kBAAkB;IASzB;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IA8BvB,eAAe;IAStB,QAAQ,CAAC,KAAK,EAAE,KAAK;IAIrB,OAAO,CAAC,IAAI,EAAE,IAAI;IAIlB,OAAO,CAAC,sBAAsB;IAiB9B,OAAO,CAAC,qBAAqB;IAYvB,GAAG,CACP,OAAO,CAAC,EAAE,UAAU,EACpB,KAAK,GAAE,aAAiC,EACxC,MAAM,GAAE,OAAe,EACvB,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,IAAI,CAAC;IAmNhB,OAAO,CAAC,eAAe;IAOvB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IA4BxB,kBAAkB,IAAI,MAAM;IAK5B,KAAK;IAaL,UAAU,IAAI,UAAU,EAAE;CA8C3B;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAiBtD;AAKD,eAAO,MAAM,SAAS,OAAoB,CAAC;AAG3C,wBAAgB,eAAe,UAE9B;AAED,wBAAgB,YAAY,SAK3B"}
|
package/dist/esm/suite.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { unriftGlobalContext } from "./context.js";
|
|
2
2
|
import { resetMatchers } from "./matchers.js";
|
|
3
3
|
import { TaskMode, TaskStatus } from "./types.js";
|
|
4
|
+
import { regexTest } from "./utils/helpers.js";
|
|
4
5
|
import { toError } from "./utils/toError.js";
|
|
5
6
|
function appendHookFailure(existing, hookName, hookError) {
|
|
6
7
|
if (!existing)
|
|
@@ -42,6 +43,34 @@ export class Suite {
|
|
|
42
43
|
for (const suite of this.suites)
|
|
43
44
|
suite.markSubtreeSkipped();
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Used when --grep prunes a suite. Matching todos stay todo so their status
|
|
48
|
+
* doesn't depend on whether a runnable sibling matched; everything else is
|
|
49
|
+
* skipped.
|
|
50
|
+
*/
|
|
51
|
+
markSubtreeFilteredOut(grep, isOnly, inOnlyContext) {
|
|
52
|
+
for (const test of this.tests) {
|
|
53
|
+
if (test.status !== TaskStatus.Pending)
|
|
54
|
+
continue;
|
|
55
|
+
test.status =
|
|
56
|
+
test.mode === TaskMode.Todo && this.testMatchesGrep(test, grep)
|
|
57
|
+
? TaskStatus.Todo
|
|
58
|
+
: TaskStatus.Skipped;
|
|
59
|
+
test.durationMs = 0;
|
|
60
|
+
}
|
|
61
|
+
// Same skip and .only rules as run(), so a suite excluded by .only stays
|
|
62
|
+
// skipped no matter which pattern pruned its parent.
|
|
63
|
+
for (const suite of this.suites) {
|
|
64
|
+
const childOnly = inOnlyContext || suite.mode === TaskMode.Only;
|
|
65
|
+
if (suite.mode === TaskMode.Skip ||
|
|
66
|
+
(isOnly && !suite.subtreeHasOnly && !childOnly)) {
|
|
67
|
+
suite.markSubtreeSkipped();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
suite.markSubtreeFilteredOut(grep, isOnly, childOnly);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
45
74
|
markSubtreeTodo() {
|
|
46
75
|
for (const test of this.tests) {
|
|
47
76
|
if (test.status === TaskStatus.Pending)
|
|
@@ -100,6 +129,12 @@ export class Suite {
|
|
|
100
129
|
this.markSubtreeSkipped();
|
|
101
130
|
return;
|
|
102
131
|
}
|
|
132
|
+
// Same for --grep: nothing below can run, so don't run hooks either
|
|
133
|
+
if (options?.grep &&
|
|
134
|
+
!this.hasRunnableMatch(options.grep, isOnly, inOnlyContext)) {
|
|
135
|
+
this.markSubtreeFilteredOut(options.grep, isOnly, inOnlyContext);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
103
138
|
let entered = false;
|
|
104
139
|
try {
|
|
105
140
|
// beforeAll
|
|
@@ -122,6 +157,12 @@ export class Suite {
|
|
|
122
157
|
const test = this.tests[i];
|
|
123
158
|
if (state.bailed)
|
|
124
159
|
break;
|
|
160
|
+
// Checked before skip/todo so a non-matching todo reports as skipped
|
|
161
|
+
if (options?.grep && !this.testMatchesGrep(test, options.grep)) {
|
|
162
|
+
test.status = TaskStatus.Skipped;
|
|
163
|
+
test.durationMs = 0;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
125
166
|
if (test.mode === TaskMode.Skip) {
|
|
126
167
|
test.status = TaskStatus.Skipped;
|
|
127
168
|
test.durationMs = 0;
|
|
@@ -139,32 +180,59 @@ export class Suite {
|
|
|
139
180
|
test.durationMs = 0;
|
|
140
181
|
continue;
|
|
141
182
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
183
|
+
// Each attempt gets its own beforeEach/afterEach so state resets between retries.
|
|
184
|
+
const maxAttempts = (test.retries ?? options?.retries ?? 0) + 1;
|
|
185
|
+
let totalMs = 0;
|
|
186
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
187
|
+
test.attempts = attempt;
|
|
188
|
+
test.error = undefined;
|
|
189
|
+
// Reset so an attempt whose beforeEach throws doesn't report the
|
|
190
|
+
// previous attempt's time.
|
|
191
|
+
test.durationMs = 0;
|
|
192
|
+
try {
|
|
193
|
+
for (const hook of beforeEachHooks) {
|
|
194
|
+
try {
|
|
195
|
+
await hook();
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw appendHookFailure(undefined, "beforeEach", toError(error));
|
|
199
|
+
}
|
|
149
200
|
}
|
|
201
|
+
await test.run(options?.timeoutMs);
|
|
150
202
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
203
|
+
catch (error) {
|
|
204
|
+
test.status = TaskStatus.Fail;
|
|
205
|
+
test.error = toError(error);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
for (const hook of afterEachHooks) {
|
|
209
|
+
try {
|
|
210
|
+
await hook();
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
test.status = TaskStatus.Fail;
|
|
214
|
+
test.error = appendHookFailure(test.error, "afterEach", toError(error));
|
|
215
|
+
}
|
|
161
216
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
217
|
+
}
|
|
218
|
+
totalMs += test.durationMs;
|
|
219
|
+
if (test.status !== TaskStatus.Fail)
|
|
220
|
+
break;
|
|
221
|
+
// A timed-out body can't be cancelled and may still be running, so
|
|
222
|
+
// another attempt could race with it. Stop here instead.
|
|
223
|
+
if (test.timedOut) {
|
|
224
|
+
if (attempt < maxAttempts && test.error) {
|
|
225
|
+
const note = "\n\n[retries] not retried: the timed-out attempt may still be running";
|
|
226
|
+
// Read the stack before touching the message: V8 builds it lazily,
|
|
227
|
+
// and an afterEach failure may have already combined it.
|
|
228
|
+
const stack = test.error.stack ?? test.error.message;
|
|
229
|
+
test.error.message += note;
|
|
230
|
+
test.error.stack = stack + note;
|
|
165
231
|
}
|
|
232
|
+
break;
|
|
166
233
|
}
|
|
167
234
|
}
|
|
235
|
+
test.durationMs = totalMs;
|
|
168
236
|
// Mark everything that hasn't run as skipped on bail
|
|
169
237
|
if (options?.bail && test.status === TaskStatus.Fail) {
|
|
170
238
|
state.bailed = true;
|
|
@@ -212,6 +280,31 @@ export class Suite {
|
|
|
212
280
|
}
|
|
213
281
|
}
|
|
214
282
|
}
|
|
283
|
+
testMatchesGrep(test, grep) {
|
|
284
|
+
return regexTest(grep, `${this.getFullDescription()} › ${test.description}`);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* True if a test below this suite matches grep AND would actually run, using
|
|
288
|
+
* the same skip/todo/only rules as run(). A matching test that won't run
|
|
289
|
+
* shouldn't cause this suite's hooks to fire.
|
|
290
|
+
*/
|
|
291
|
+
hasRunnableMatch(grep, isOnly, inOnlyContext) {
|
|
292
|
+
const runnable = this.tests.some((test) => test.mode !== TaskMode.Skip &&
|
|
293
|
+
test.mode !== TaskMode.Todo &&
|
|
294
|
+
(!isOnly || inOnlyContext || test.mode === TaskMode.Only) &&
|
|
295
|
+
this.testMatchesGrep(test, grep));
|
|
296
|
+
if (runnable)
|
|
297
|
+
return true;
|
|
298
|
+
return this.suites.some((suite) => {
|
|
299
|
+
if (suite.mode === TaskMode.Skip || suite.mode === TaskMode.Todo) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const childOnly = inOnlyContext || suite.mode === TaskMode.Only;
|
|
303
|
+
if (isOnly && !suite.subtreeHasOnly && !childOnly)
|
|
304
|
+
return false;
|
|
305
|
+
return suite.hasRunnableMatch(grep, isOnly, childOnly);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
215
308
|
getFullDescription() {
|
|
216
309
|
if (!this.parent || this.parent.root)
|
|
217
310
|
return this.description;
|
|
@@ -261,6 +354,7 @@ export class Suite {
|
|
|
261
354
|
status: test.status,
|
|
262
355
|
error: test.error,
|
|
263
356
|
durationMs: test.durationMs ?? 0,
|
|
357
|
+
attempts: test.attempts,
|
|
264
358
|
});
|
|
265
359
|
}
|
|
266
360
|
for (const child of this.suites) {
|
package/dist/esm/test.d.ts
CHANGED
|
@@ -7,7 +7,11 @@ export declare class Test {
|
|
|
7
7
|
mode: TaskMode;
|
|
8
8
|
status: TaskStatus;
|
|
9
9
|
timeoutMs?: number;
|
|
10
|
-
|
|
10
|
+
retries?: number;
|
|
11
|
+
attempts: number;
|
|
12
|
+
/** True if the last run timed out. Its body may still be running. */
|
|
13
|
+
timedOut: boolean;
|
|
14
|
+
constructor(description: string, fn: PromisableFn<void>, mode?: TaskMode, timeoutMs?: number, retries?: number);
|
|
11
15
|
run(globalTimeoutMs?: number): Promise<void>;
|
|
12
16
|
}
|
|
13
17
|
//# sourceMappingURL=test.d.ts.map
|
package/dist/esm/test.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../src/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../src/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AASlE,qBAAa,IAAI;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAK;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IAEd,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,EAAE,QAAQ,CAAC;IAEf,MAAM,EAAE,UAAU,CAAsB;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAK;IACrB,qEAAqE;IACrE,QAAQ,EAAE,OAAO,CAAS;gBAGxB,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE,QAA2B,EACjC,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM;IASZ,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA2DnD"}
|
package/dist/esm/test.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { TaskMode, TaskStatus } from "./types.js";
|
|
2
2
|
import { toError } from "./utils/toError.js";
|
|
3
|
+
/**
|
|
4
|
+
* Captured at module load so the runner's own timing measurements are
|
|
5
|
+
* immune to user mocks (e.g. mock.useFakeTimers patches performance.now).
|
|
6
|
+
*/
|
|
7
|
+
const realPerformanceNow = performance.now.bind(performance);
|
|
3
8
|
export class Test {
|
|
4
9
|
description;
|
|
5
10
|
durationMs = 0;
|
|
@@ -8,11 +13,16 @@ export class Test {
|
|
|
8
13
|
mode;
|
|
9
14
|
status = TaskStatus.Pending;
|
|
10
15
|
timeoutMs;
|
|
11
|
-
|
|
16
|
+
retries;
|
|
17
|
+
attempts = 0;
|
|
18
|
+
/** True if the last run timed out. Its body may still be running. */
|
|
19
|
+
timedOut = false;
|
|
20
|
+
constructor(description, fn, mode = TaskMode.Default, timeoutMs, retries) {
|
|
12
21
|
this.description = description;
|
|
13
22
|
this.fn = fn;
|
|
14
23
|
this.mode = mode;
|
|
15
24
|
this.timeoutMs = timeoutMs;
|
|
25
|
+
this.retries = retries;
|
|
16
26
|
}
|
|
17
27
|
async run(globalTimeoutMs) {
|
|
18
28
|
const timeoutMs = this.timeoutMs ?? globalTimeoutMs;
|
|
@@ -26,20 +36,24 @@ export class Test {
|
|
|
26
36
|
this.durationMs = 0;
|
|
27
37
|
return;
|
|
28
38
|
}
|
|
29
|
-
const start =
|
|
39
|
+
const start = realPerformanceNow();
|
|
30
40
|
this.status = TaskStatus.Running;
|
|
41
|
+
this.timedOut = false;
|
|
31
42
|
const exec = Promise.resolve().then(() => this.fn());
|
|
32
43
|
try {
|
|
33
44
|
if (timeoutMs !== undefined) {
|
|
34
45
|
let timeoutId = null;
|
|
35
46
|
const timeoutFn = new Promise((_, reject) => {
|
|
36
47
|
timeoutId = setTimeout(() => {
|
|
48
|
+
this.timedOut = true;
|
|
37
49
|
reject(new Error(`Test "${this.description}" timed out after ${timeoutMs} ms`));
|
|
38
50
|
}, timeoutMs);
|
|
39
51
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
/**
|
|
53
|
+
* NOTE: Promise.race does not cancel the losing promise. A timed-out test
|
|
54
|
+
* function continues running in the background. True cancellation would
|
|
55
|
+
* require AbortController integration, which is a larger future change.
|
|
56
|
+
*/
|
|
43
57
|
try {
|
|
44
58
|
await Promise.race([exec, timeoutFn]);
|
|
45
59
|
}
|
|
@@ -58,7 +72,7 @@ export class Test {
|
|
|
58
72
|
this.status = TaskStatus.Fail;
|
|
59
73
|
}
|
|
60
74
|
finally {
|
|
61
|
-
this.durationMs =
|
|
75
|
+
this.durationMs = realPerformanceNow() - start;
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
78
|
}
|
|
@@ -142,7 +142,7 @@ export function compareInternal(left, right, options, leftToRightSeen, rightToLe
|
|
|
142
142
|
const rightSet = currentRight;
|
|
143
143
|
if (leftSet.size !== rightSet.size)
|
|
144
144
|
return false;
|
|
145
|
-
// Fast path: primitive-only sets can use has() directly
|
|
145
|
+
// Fast path: primitive-only sets can use has() directly - O(n) vs O(n²)
|
|
146
146
|
let allPrimitive = true;
|
|
147
147
|
for (const v of leftSet) {
|
|
148
148
|
if (!isPrimitive(v)) {
|
|
@@ -179,7 +179,7 @@ export function compareInternal(left, right, options, leftToRightSeen, rightToLe
|
|
|
179
179
|
const rightMap = currentRight;
|
|
180
180
|
if (leftMap.size !== rightMap.size)
|
|
181
181
|
return false;
|
|
182
|
-
// Fast path: primitive-only keys can use get() directly
|
|
182
|
+
// Fast path: primitive-only keys can use get() directly - O(n) vs O(n²)
|
|
183
183
|
let allKeysPrimitive = true;
|
|
184
184
|
for (const k of leftMap.keys()) {
|
|
185
185
|
if (!isPrimitive(k)) {
|
|
@@ -230,7 +230,7 @@ export function compareInternal(left, right, options, leftToRightSeen, rightToLe
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
else {
|
|
233
|
-
// Prototype-insensitive (looseEqual)
|
|
233
|
+
// Prototype-insensitive (looseEqual) - union-of-keys, missing ≈ undefined
|
|
234
234
|
const leftKeys = Object.keys(currentLeft);
|
|
235
235
|
const rightKeys = Object.keys(currentRight);
|
|
236
236
|
const allKeys = new Set([...leftKeys, ...rightKeys]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defineConfig.d.ts","sourceRoot":"","sources":["../../../src/utils/defineConfig.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,mBAAmB,GAC3B,mBAAmB,CAErB"}
|
|
1
|
+
{"version":3,"file":"defineConfig.d.ts","sourceRoot":"","sources":["../../../src/utils/defineConfig.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,mBAAmB,GAC3B,mBAAmB,CAErB"}
|
|
@@ -14,11 +14,23 @@ export declare function isGlobPattern(pattern: string): boolean;
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function globToRegex(glob: string): RegExp;
|
|
16
16
|
export declare function safeRegExp(source: string): RegExp;
|
|
17
|
+
/**
|
|
18
|
+
* Stateless RegExp match for user-supplied patterns.
|
|
19
|
+
*
|
|
20
|
+
* `RegExp.prototype.test` advances `lastIndex` for `/g` and `/y` regexes, so
|
|
21
|
+
* reusing the same regex object across calls (a matcher tested against several
|
|
22
|
+
* requests, or the same assertion run twice) can start mid-string and miss.
|
|
23
|
+
* Resetting `lastIndex` first makes every test independent - "does this pattern
|
|
24
|
+
* match the string" regardless of prior calls.
|
|
25
|
+
*/
|
|
26
|
+
export declare function regexTest(re: RegExp, value: string): boolean;
|
|
17
27
|
/**
|
|
18
28
|
* SameValueZero-ish semantics:
|
|
19
29
|
* - NaN equals NaN
|
|
20
30
|
* - +0 and -0 are equal
|
|
21
31
|
*/
|
|
22
32
|
export declare function sameValue(a: unknown, b: unknown): boolean;
|
|
33
|
+
/** Throws unless `retries` is undefined or a non-negative integer. */
|
|
34
|
+
export declare function assertValidRetries(retries: unknown, where: string): void;
|
|
23
35
|
export {};
|
|
24
36
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA,KAAK,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhF,MAAM,MAAM,UAAU,GAClB,UAAU,GACV,SAAS,GACT,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,cAAc,CAAC;AAEnB,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAY9D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyDhD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CASzD"}
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA,KAAK,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhF,MAAM,MAAM,UAAU,GAClB,UAAU,GACV,SAAS,GACT,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,cAAc,CAAC;AAEnB,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAY9D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyDhD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAG5D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CASzD;AAED,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CASxE"}
|
|
@@ -98,6 +98,19 @@ export function safeRegExp(source) {
|
|
|
98
98
|
throw new Error(`Invalid regex "${source}": ${msg}`);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Stateless RegExp match for user-supplied patterns.
|
|
103
|
+
*
|
|
104
|
+
* `RegExp.prototype.test` advances `lastIndex` for `/g` and `/y` regexes, so
|
|
105
|
+
* reusing the same regex object across calls (a matcher tested against several
|
|
106
|
+
* requests, or the same assertion run twice) can start mid-string and miss.
|
|
107
|
+
* Resetting `lastIndex` first makes every test independent - "does this pattern
|
|
108
|
+
* match the string" regardless of prior calls.
|
|
109
|
+
*/
|
|
110
|
+
export function regexTest(re, value) {
|
|
111
|
+
re.lastIndex = 0;
|
|
112
|
+
return re.test(value);
|
|
113
|
+
}
|
|
101
114
|
/**
|
|
102
115
|
* SameValueZero-ish semantics:
|
|
103
116
|
* - NaN equals NaN
|
|
@@ -111,3 +124,10 @@ export function sameValue(a, b) {
|
|
|
111
124
|
Number.isNaN(a) &&
|
|
112
125
|
Number.isNaN(b));
|
|
113
126
|
}
|
|
127
|
+
/** Throws unless `retries` is undefined or a non-negative integer. */
|
|
128
|
+
export function assertValidRetries(retries, where) {
|
|
129
|
+
if (retries !== undefined &&
|
|
130
|
+
!(typeof retries === "number" && Number.isInteger(retries) && retries >= 0)) {
|
|
131
|
+
throw new TypeError(`${where}: retries must be a non-negative integer, got ${typeof retries === "string" ? JSON.stringify(retries) : String(retries)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toThrow.d.ts","sourceRoot":"","sources":["../../../src/utils/toThrow.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"toThrow.d.ts","sourceRoot":"","sources":["../../../src/utils/toThrow.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,MAAM,GACN,MAAM,GACN,CAAC,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,GACnC;IAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,UAAU,WAAW;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,MAAM,CAAC;CACvB;AAgED,wBAAgB,OAAO,CACrB,QAAQ,EAAE,OAAO,EACjB,QAAQ,CAAC,EAAE,eAAe,EAC1B,KAAK,UAAQ,GACZ,WAAW,CAgEb"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { regexTest } from "./helpers.js";
|
|
1
2
|
function isErrorConstructor(err) {
|
|
2
3
|
return typeof err === "function";
|
|
3
4
|
}
|
|
@@ -24,7 +25,7 @@ function matchThrown(thrown, expected) {
|
|
|
24
25
|
}
|
|
25
26
|
// toThrow(/regex/)
|
|
26
27
|
if (expected instanceof RegExp) {
|
|
27
|
-
return expected
|
|
28
|
+
return regexTest(expected, msg);
|
|
28
29
|
}
|
|
29
30
|
// toThrow(ErrorClass)
|
|
30
31
|
if (isErrorConstructor(expected)) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unrift/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Core engine for the Unrift TypeScript test framework
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Core engine for the Unrift TypeScript test framework - runner, matchers, CLI, and programmatic API",
|
|
5
5
|
"private": false,
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "./dist/cjs/index.cjs",
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"access": "public"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"esbuild": "^0.27.2"
|
|
61
|
+
"esbuild": "^0.27.2",
|
|
62
|
+
"memfs": "^4.57.7"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"ts-node": "^10.9.2",
|