@unrift/core 0.2.1 → 0.4.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 -13
- package/dist/esm/context.d.ts +1 -2
- package/dist/esm/context.d.ts.map +1 -1
- package/dist/esm/expect.d.ts +21 -1
- package/dist/esm/expect.d.ts.map +1 -1
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/interface.d.ts +3 -3
- package/dist/esm/interface.d.ts.map +1 -1
- package/dist/esm/interface.js +11 -6
- package/dist/esm/matchers.d.ts.map +1 -1
- package/dist/esm/matchers.js +261 -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/runner.d.ts +1 -0
- package/dist/esm/runner.d.ts.map +1 -1
- package/dist/esm/runner.js +138 -28
- package/dist/esm/test.d.ts +3 -2
- package/dist/esm/test.d.ts.map +1 -1
- package/dist/esm/test.js +17 -7
- package/dist/esm/types.d.ts +0 -1
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/types.js +0 -1
- package/dist/esm/utils/compareEngine.d.ts.map +1 -1
- package/dist/esm/utils/compareEngine.js +32 -1
- package/dist/esm/utils/defineConfig.d.ts +0 -1
- package/dist/esm/utils/defineConfig.d.ts.map +1 -1
- package/dist/esm/utils/diff.d.ts.map +1 -1
- package/dist/esm/utils/diff.js +31 -0
- package/dist/esm/utils/discoverTestFiles.js +1 -1
- package/dist/esm/utils/helpers.d.ts +21 -0
- package/dist/esm/utils/helpers.d.ts.map +1 -1
- package/dist/esm/utils/helpers.js +97 -0
- package/dist/esm/utils/loadConfig.d.ts.map +1 -1
- package/dist/esm/utils/loadConfig.js +2 -2
- package/dist/esm/utils/toThrow.d.ts.map +1 -1
- package/dist/esm/utils/toThrow.js +2 -1
- package/package.json +8 -3
package/dist/esm/runner.js
CHANGED
|
@@ -1,38 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { watch as fsWatch, readdirSync } from "node:fs";
|
|
2
|
+
import { relative, resolve, join } from "node:path";
|
|
2
3
|
import { loadConfig, loadConfigFromPath, } from "./utils/loadConfig.js";
|
|
3
4
|
import { cleanUnriftCaches } from "./utils/transform.js";
|
|
4
5
|
import { colors } from "./utils/colors.js";
|
|
5
6
|
import { discoverTestFiles } from "./utils/discoverTestFiles.js";
|
|
6
7
|
import { runEngine } from "./run.js";
|
|
7
8
|
import { normalizePath } from "./utils/normalizePath.js";
|
|
9
|
+
import { safeRegExp, isGlobPattern, globToRegex } from "./utils/helpers.js";
|
|
8
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);
|
|
9
16
|
function debugLog(enabled, json, ...args) {
|
|
10
17
|
// Keep stdout clean for --json mode; use stderr for debug.
|
|
11
18
|
if (!enabled)
|
|
12
19
|
return;
|
|
13
20
|
(json ? console.error : console.log)(...args);
|
|
14
21
|
}
|
|
15
|
-
function safeRegExp(source) {
|
|
16
|
-
try {
|
|
17
|
-
return new RegExp(source);
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
21
|
-
throw new Error(`Invalid regex "${source}": ${msg}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
22
|
const regExpCache = new Map();
|
|
25
23
|
function getCachedRegExp(pattern) {
|
|
26
24
|
let re = regExpCache.get(pattern);
|
|
27
25
|
if (!re) {
|
|
28
|
-
re = safeRegExp(pattern);
|
|
26
|
+
re = isGlobPattern(pattern) ? globToRegex(pattern) : safeRegExp(pattern);
|
|
29
27
|
regExpCache.set(pattern, re);
|
|
30
28
|
}
|
|
31
29
|
return re;
|
|
32
30
|
}
|
|
33
|
-
/**
|
|
34
|
-
* @TODO Add glob support
|
|
35
|
-
*/
|
|
36
31
|
function matchesAnyPattern(fileAbs, patterns, baseDir) {
|
|
37
32
|
const absolutePath = normalizePath(fileAbs);
|
|
38
33
|
const relativePath = normalizePath(relative(baseDir, fileAbs));
|
|
@@ -51,6 +46,32 @@ function filterByIncludesExcludes(files, baseDir, includes, excludes) {
|
|
|
51
46
|
}
|
|
52
47
|
return filtered;
|
|
53
48
|
}
|
|
49
|
+
function filterStack(stack) {
|
|
50
|
+
const lines = stack.split("\n");
|
|
51
|
+
const message = [];
|
|
52
|
+
const frames = [];
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (line.trimStart().startsWith("at ")) {
|
|
55
|
+
frames.push(line);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
message.push(line);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const filtered = frames.filter((line) => {
|
|
62
|
+
if (line.includes("node:"))
|
|
63
|
+
return false;
|
|
64
|
+
if (line.includes("/@unrift/core/dist/"))
|
|
65
|
+
return false;
|
|
66
|
+
if (line.includes("/unrift/dist/"))
|
|
67
|
+
return false;
|
|
68
|
+
if (line.includes("/dist/esm/"))
|
|
69
|
+
return false;
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
const kept = filtered.length > 0 ? filtered : frames;
|
|
73
|
+
return [...message, ...kept].join("\n");
|
|
74
|
+
}
|
|
54
75
|
function formatDuration(ms) {
|
|
55
76
|
if (ms < 1000)
|
|
56
77
|
return `${ms.toFixed(0)}ms`;
|
|
@@ -127,7 +148,8 @@ function extractTestName(description) {
|
|
|
127
148
|
return description.replace(/^.*?›\s*/, "");
|
|
128
149
|
}
|
|
129
150
|
export async function runTestsCLI(options = {}) {
|
|
130
|
-
|
|
151
|
+
regExpCache.clear();
|
|
152
|
+
const runStart = realPerformanceNow();
|
|
131
153
|
// Cache clean mode
|
|
132
154
|
if (options.cacheClean) {
|
|
133
155
|
const projectRoot = process.cwd();
|
|
@@ -142,7 +164,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
142
164
|
if (result.tempCacheDeleted)
|
|
143
165
|
lines.push("✔ deleted temp cache");
|
|
144
166
|
if (lines.length === 0)
|
|
145
|
-
lines.push("
|
|
167
|
+
lines.push("- nothing to clean");
|
|
146
168
|
console.log(lines.join("\n"));
|
|
147
169
|
}
|
|
148
170
|
return;
|
|
@@ -162,8 +184,10 @@ export async function runTestsCLI(options = {}) {
|
|
|
162
184
|
// Test dir is relative to config dir if config is loaded
|
|
163
185
|
const testDir = resolve(configDir, config?.testDir ?? options.testDir ?? "test");
|
|
164
186
|
let files = discoverTestFiles(testDir);
|
|
165
|
-
|
|
166
|
-
|
|
187
|
+
const pattern = options.pattern ??
|
|
188
|
+
(config?.pattern ? safeRegExp(config.pattern) : undefined);
|
|
189
|
+
if (pattern) {
|
|
190
|
+
files = files.filter((f) => pattern.test(normalizePath(f)));
|
|
167
191
|
}
|
|
168
192
|
files = filterByIncludesExcludes(files, testDir, config?.includes, config?.excludes).sort((a, b) => normalizePath(a).localeCompare(normalizePath(b)));
|
|
169
193
|
debugLog(options.debug, options.json, "Discovered test files:", files);
|
|
@@ -196,7 +220,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
196
220
|
testDir: dir,
|
|
197
221
|
configPath: resolvedConfigPath,
|
|
198
222
|
files: [],
|
|
199
|
-
durationMs:
|
|
223
|
+
durationMs: realPerformanceNow() - runStart,
|
|
200
224
|
results: [],
|
|
201
225
|
};
|
|
202
226
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -246,7 +270,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
246
270
|
});
|
|
247
271
|
}
|
|
248
272
|
const ok = failed === 0;
|
|
249
|
-
const durationMs =
|
|
273
|
+
const durationMs = realPerformanceNow() - runStart;
|
|
250
274
|
// JSON mode: stdout must be JSON only
|
|
251
275
|
if (options.json) {
|
|
252
276
|
const report = {
|
|
@@ -269,7 +293,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
269
293
|
process.exitCode = 1;
|
|
270
294
|
return;
|
|
271
295
|
}
|
|
272
|
-
//
|
|
296
|
+
// Pretty reporting
|
|
273
297
|
const groups = groupResultsByFile(results);
|
|
274
298
|
// File-by-file results
|
|
275
299
|
for (const group of groups) {
|
|
@@ -296,14 +320,13 @@ export async function runTestsCLI(options = {}) {
|
|
|
296
320
|
const parts = testName.split(" › ");
|
|
297
321
|
const leaf = parts[parts.length - 1];
|
|
298
322
|
const suitePath = parts.slice(0, -1).join(" › ");
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
323
|
+
const depth = parts.length - 1;
|
|
324
|
+
// Print suite name when it changes (depth > 0 guards against top-level it() tests)
|
|
325
|
+
if (depth > 0 && suitePath && suitePath !== lastSuitePath) {
|
|
302
326
|
const suiteIndent = " " + " ".repeat(Math.max(0, depth - 1));
|
|
303
327
|
console.log(`${suiteIndent}${colors.dim("›")} ${colors.bold(parts[depth - 1])}`);
|
|
304
328
|
lastSuitePath = suitePath;
|
|
305
329
|
}
|
|
306
|
-
const depth = parts.length - 1;
|
|
307
330
|
const indent = " " + " ".repeat(depth);
|
|
308
331
|
if (result.status === TaskStatus.Pass) {
|
|
309
332
|
const dur = colors.dim(formatDuration(result.durationMs ?? 0));
|
|
@@ -325,14 +348,14 @@ export async function runTestsCLI(options = {}) {
|
|
|
325
348
|
}
|
|
326
349
|
}
|
|
327
350
|
}
|
|
328
|
-
//
|
|
351
|
+
// Failure details
|
|
329
352
|
if (failures.length > 0) {
|
|
330
353
|
console.log(`\n${horizontalRule(colors.boldRed(" FAILURES "))}\n`);
|
|
331
354
|
failures.forEach((r, i) => {
|
|
332
355
|
const testName = extractTestName(r.description);
|
|
333
356
|
console.log(colors.boldRed(` ${i + 1}) ${testName}`));
|
|
334
357
|
if (r.error) {
|
|
335
|
-
const errText = r.error.stack ?? r.error.message;
|
|
358
|
+
const errText = filterStack(r.error.stack ?? r.error.message);
|
|
336
359
|
const lines = errText.split("\n");
|
|
337
360
|
for (const line of lines) {
|
|
338
361
|
// Highlight the "Received/Expected" lines differently
|
|
@@ -355,7 +378,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
355
378
|
});
|
|
356
379
|
console.log(`\n${horizontalRule()}`);
|
|
357
380
|
}
|
|
358
|
-
//
|
|
381
|
+
// Summary
|
|
359
382
|
console.log("");
|
|
360
383
|
const summaryParts = [];
|
|
361
384
|
if (passed > 0)
|
|
@@ -373,3 +396,90 @@ export async function runTestsCLI(options = {}) {
|
|
|
373
396
|
if (!ok)
|
|
374
397
|
process.exitCode = 1;
|
|
375
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Watch a directory tree for changes, calling `onChange` after a debounce.
|
|
401
|
+
* Uses individual `fs.watch()` calls per directory so it works on all
|
|
402
|
+
* platforms (Linux `fs.watch` does not support `recursive: true` before Node 22).
|
|
403
|
+
* Returns a cleanup function to stop watching.
|
|
404
|
+
*/
|
|
405
|
+
function watchRecursive(dir, onChange, debounceMs = 150) {
|
|
406
|
+
let timer = null;
|
|
407
|
+
let lastFile = "";
|
|
408
|
+
function trigger(filename) {
|
|
409
|
+
lastFile = filename;
|
|
410
|
+
if (timer)
|
|
411
|
+
clearTimeout(timer);
|
|
412
|
+
timer = setTimeout(() => onChange(lastFile), debounceMs);
|
|
413
|
+
}
|
|
414
|
+
const watchers = [];
|
|
415
|
+
function watchDir(d) {
|
|
416
|
+
try {
|
|
417
|
+
watchers.push(fsWatch(d, (_event, filename) => trigger(filename ?? d)));
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
let entries;
|
|
423
|
+
try {
|
|
424
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
for (const entry of entries) {
|
|
430
|
+
if (entry.isDirectory() &&
|
|
431
|
+
!entry.name.startsWith(".") &&
|
|
432
|
+
entry.name !== "node_modules") {
|
|
433
|
+
watchDir(join(d, entry.name));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
watchDir(dir);
|
|
438
|
+
return () => {
|
|
439
|
+
if (timer)
|
|
440
|
+
clearTimeout(timer);
|
|
441
|
+
for (const w of watchers) {
|
|
442
|
+
try {
|
|
443
|
+
w.close();
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// ignore
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
export async function watchTestsCLI(options = {}) {
|
|
452
|
+
// Run once immediately
|
|
453
|
+
await runTestsCLI(options);
|
|
454
|
+
// Resolve the directory to watch (same logic as runTestsCLI)
|
|
455
|
+
const loaded = options.configPath
|
|
456
|
+
? await import("./utils/loadConfig").then((m) => m.loadConfigFromPath(options.configPath))
|
|
457
|
+
: await import("./utils/loadConfig").then((m) => m.loadConfig(process.cwd()));
|
|
458
|
+
const config = loaded?.config ?? null;
|
|
459
|
+
const configDir = loaded?.configDir ?? process.cwd();
|
|
460
|
+
const testDir = resolve(configDir, config?.testDir ?? options.testDir ?? "test");
|
|
461
|
+
console.log(`\n ${colors.dim("↺")} ${colors.dim("Watching")} ${colors.bold(shortenPath(testDir))} ${colors.dim("for changes… Ctrl+C to quit")}\n`);
|
|
462
|
+
let running = false;
|
|
463
|
+
const stop = watchRecursive(testDir, async (filename) => {
|
|
464
|
+
if (running)
|
|
465
|
+
return;
|
|
466
|
+
running = true;
|
|
467
|
+
const shortFile = shortenPath(filename);
|
|
468
|
+
console.log(`\n${colors.dim("─".repeat(Math.min(process.stdout.columns || 80, 80)))}`);
|
|
469
|
+
console.log(` ${colors.dim("↺")} ${colors.dim(`${shortFile} changed - re-running…`)}\n`);
|
|
470
|
+
// Reset exit code so each run is evaluated independently
|
|
471
|
+
process.exitCode = 0;
|
|
472
|
+
try {
|
|
473
|
+
await runTestsCLI(options);
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
running = false;
|
|
477
|
+
}
|
|
478
|
+
console.log(` ${colors.dim("↺")} ${colors.dim("Watching for changes… Ctrl+C to quit")}\n`);
|
|
479
|
+
});
|
|
480
|
+
// Keep process alive and clean up on exit
|
|
481
|
+
process.on("SIGINT", () => {
|
|
482
|
+
stop();
|
|
483
|
+
process.exit(0);
|
|
484
|
+
});
|
|
485
|
+
}
|
package/dist/esm/test.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export declare class Test {
|
|
|
6
6
|
fn: PromisableFn<void>;
|
|
7
7
|
mode: TaskMode;
|
|
8
8
|
status: TaskStatus;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
constructor(description: string, fn: PromisableFn<void>, mode?: TaskMode, timeoutMs?: number);
|
|
11
|
+
run(globalTimeoutMs?: number): Promise<void>;
|
|
11
12
|
}
|
|
12
13
|
//# 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;gBAGjB,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE,QAA2B,EACjC,SAAS,CAAC,EAAE,MAAM;IAQd,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAyDnD"}
|
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;
|
|
@@ -7,12 +12,15 @@ export class Test {
|
|
|
7
12
|
fn;
|
|
8
13
|
mode;
|
|
9
14
|
status = TaskStatus.Pending;
|
|
10
|
-
|
|
15
|
+
timeoutMs;
|
|
16
|
+
constructor(description, fn, mode = TaskMode.Default, timeoutMs) {
|
|
11
17
|
this.description = description;
|
|
12
18
|
this.fn = fn;
|
|
13
19
|
this.mode = mode;
|
|
20
|
+
this.timeoutMs = timeoutMs;
|
|
14
21
|
}
|
|
15
|
-
async run(
|
|
22
|
+
async run(globalTimeoutMs) {
|
|
23
|
+
const timeoutMs = this.timeoutMs ?? globalTimeoutMs;
|
|
16
24
|
if (this.mode === TaskMode.Skip) {
|
|
17
25
|
this.status = TaskStatus.Skipped;
|
|
18
26
|
this.durationMs = 0;
|
|
@@ -23,7 +31,7 @@ export class Test {
|
|
|
23
31
|
this.durationMs = 0;
|
|
24
32
|
return;
|
|
25
33
|
}
|
|
26
|
-
const start =
|
|
34
|
+
const start = realPerformanceNow();
|
|
27
35
|
this.status = TaskStatus.Running;
|
|
28
36
|
const exec = Promise.resolve().then(() => this.fn());
|
|
29
37
|
try {
|
|
@@ -34,9 +42,11 @@ export class Test {
|
|
|
34
42
|
reject(new Error(`Test "${this.description}" timed out after ${timeoutMs} ms`));
|
|
35
43
|
}, timeoutMs);
|
|
36
44
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
/**
|
|
46
|
+
* NOTE: Promise.race does not cancel the losing promise. A timed-out test
|
|
47
|
+
* function continues running in the background. True cancellation would
|
|
48
|
+
* require AbortController integration, which is a larger future change.
|
|
49
|
+
*/
|
|
40
50
|
try {
|
|
41
51
|
await Promise.race([exec, timeoutFn]);
|
|
42
52
|
}
|
|
@@ -55,7 +65,7 @@ export class Test {
|
|
|
55
65
|
this.status = TaskStatus.Fail;
|
|
56
66
|
}
|
|
57
67
|
finally {
|
|
58
|
-
this.durationMs =
|
|
68
|
+
this.durationMs = realPerformanceNow() - start;
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
}
|
package/dist/esm/types.d.ts
CHANGED
package/dist/esm/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAE3C,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC;AAElD,oBAAY,UAAU;IACpB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAE3C,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC;AAElD,oBAAY,UAAU;IACpB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,IAAI,SAAS;CACd;AAED,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,IAAI,SAAS;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,SAAS,GAAG,CACtB,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,OAAO,EACjB,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC"}
|
package/dist/esm/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compareEngine.d.ts","sourceRoot":"","sources":["../../../src/utils/compareEngine.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,cAAc,EAAE,OAAO,CAAC;IACxB,0EAA0E;IAC1E,sBAAsB,EAAE,OAAO,CAAC;IAChC,yDAAyD;IACzD,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAID,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,cAAc,EACvB,eAAe,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,eAAe,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,GACxC,OAAO,
|
|
1
|
+
{"version":3,"file":"compareEngine.d.ts","sourceRoot":"","sources":["../../../src/utils/compareEngine.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,cAAc,EAAE,OAAO,CAAC;IACxB,0EAA0E;IAC1E,sBAAsB,EAAE,OAAO,CAAC;IAChC,yDAAyD;IACzD,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAID,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,cAAc,EACvB,eAAe,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,eAAe,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,GACxC,OAAO,CA4TT"}
|
|
@@ -142,6 +142,21 @@ 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 - O(n) vs O(n²)
|
|
146
|
+
let allPrimitive = true;
|
|
147
|
+
for (const v of leftSet) {
|
|
148
|
+
if (!isPrimitive(v)) {
|
|
149
|
+
allPrimitive = false;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (allPrimitive) {
|
|
154
|
+
for (const v of leftSet) {
|
|
155
|
+
if (!rightSet.has(v))
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
145
160
|
const remaining = new Set(rightSet);
|
|
146
161
|
outer: for (const leftValue of leftSet) {
|
|
147
162
|
for (const rightValue of remaining) {
|
|
@@ -164,6 +179,22 @@ export function compareInternal(left, right, options, leftToRightSeen, rightToLe
|
|
|
164
179
|
const rightMap = currentRight;
|
|
165
180
|
if (leftMap.size !== rightMap.size)
|
|
166
181
|
return false;
|
|
182
|
+
// Fast path: primitive-only keys can use get() directly - O(n) vs O(n²)
|
|
183
|
+
let allKeysPrimitive = true;
|
|
184
|
+
for (const k of leftMap.keys()) {
|
|
185
|
+
if (!isPrimitive(k)) {
|
|
186
|
+
allKeysPrimitive = false;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (allKeysPrimitive) {
|
|
191
|
+
for (const [leftKey, leftValue] of leftMap.entries()) {
|
|
192
|
+
if (!rightMap.has(leftKey))
|
|
193
|
+
return false;
|
|
194
|
+
worklist.push([leftValue, rightMap.get(leftKey)]);
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
167
198
|
const remainingEntries = new Set(rightMap.entries());
|
|
168
199
|
outer: for (const [leftKey, leftValue] of leftMap.entries()) {
|
|
169
200
|
for (const candidate of remainingEntries) {
|
|
@@ -199,7 +230,7 @@ export function compareInternal(left, right, options, leftToRightSeen, rightToLe
|
|
|
199
230
|
}
|
|
200
231
|
}
|
|
201
232
|
else {
|
|
202
|
-
// Prototype-insensitive (looseEqual)
|
|
233
|
+
// Prototype-insensitive (looseEqual) - union-of-keys, missing ≈ undefined
|
|
203
234
|
const leftKeys = Object.keys(currentLeft);
|
|
204
235
|
const rightKeys = Object.keys(currentRight);
|
|
205
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,
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/utils/diff.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/utils/diff.ts"],"names":[],"mappings":"AAuGA,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAsCvE"}
|
package/dist/esm/utils/diff.js
CHANGED
|
@@ -37,6 +37,32 @@ function formatValue(value, indent = 2) {
|
|
|
37
37
|
}
|
|
38
38
|
return String(value);
|
|
39
39
|
}
|
|
40
|
+
function diffArrays(received, expected) {
|
|
41
|
+
const len = Math.max(received.length, expected.length);
|
|
42
|
+
const lines = [];
|
|
43
|
+
for (let i = 0; i < len; i++) {
|
|
44
|
+
const inReceived = i < received.length;
|
|
45
|
+
const inExpected = i < expected.length;
|
|
46
|
+
if (inReceived && inExpected) {
|
|
47
|
+
const rv = formatValue(received[i]);
|
|
48
|
+
const ev = formatValue(expected[i]);
|
|
49
|
+
if (rv !== ev) {
|
|
50
|
+
lines.push(colors.red(` - [${i}]: ${rv}`));
|
|
51
|
+
lines.push(colors.green(` + [${i}]: ${ev}`));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
lines.push(` [${i}]: ${rv}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (inReceived) {
|
|
58
|
+
lines.push(colors.red(` - [${i}]: ${formatValue(received[i])}`));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
lines.push(colors.green(` + [${i}]: ${formatValue(expected[i])}`));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
40
66
|
function diffObjects(received, expected) {
|
|
41
67
|
const allKeys = new Set([...Object.keys(received), ...Object.keys(expected)]);
|
|
42
68
|
const lines = [];
|
|
@@ -64,6 +90,11 @@ function diffObjects(received, expected) {
|
|
|
64
90
|
return lines.join("\n");
|
|
65
91
|
}
|
|
66
92
|
export function formatDiff(received, expected) {
|
|
93
|
+
// For arrays, provide per-element diffing
|
|
94
|
+
if (Array.isArray(received) && Array.isArray(expected)) {
|
|
95
|
+
const diff = diffArrays(received, expected);
|
|
96
|
+
return (`\n${colors.red("- Received")} / ${colors.green("+ Expected")}\n\n` + diff);
|
|
97
|
+
}
|
|
67
98
|
// For objects, provide a structural diff
|
|
68
99
|
if (received !== null &&
|
|
69
100
|
expected !== null &&
|
|
@@ -3,6 +3,27 @@ export type TypedArray = Uint8Array | Int8Array | Uint16Array | Int16Array | Uin
|
|
|
3
3
|
export declare function isPrimitive(value: unknown): value is Primitive;
|
|
4
4
|
export declare function isObjectLike(value: unknown): value is object;
|
|
5
5
|
export declare function isTypedArray(value: unknown): value is TypedArray;
|
|
6
|
+
/**
|
|
7
|
+
* Returns true if the string looks like a glob pattern (contains * ? [ { }).
|
|
8
|
+
* Patterns without these characters are treated as regex strings for backward compatibility.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isGlobPattern(pattern: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Converts a glob pattern to a RegExp.
|
|
13
|
+
* Supports: ** (any path), * (any segment), ? (single char), {a,b} (alternation), [abc] (char class).
|
|
14
|
+
*/
|
|
15
|
+
export declare function globToRegex(glob: string): RegExp;
|
|
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;
|
|
6
27
|
/**
|
|
7
28
|
* SameValueZero-ish semantics:
|
|
8
29
|
* - NaN equals NaN
|
|
@@ -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;;;;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"}
|
|
@@ -14,6 +14,103 @@ export function isObjectLike(value) {
|
|
|
14
14
|
export function isTypedArray(value) {
|
|
15
15
|
return ArrayBuffer.isView(value) && !(value instanceof DataView);
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns true if the string looks like a glob pattern (contains * ? [ { }).
|
|
19
|
+
* Patterns without these characters are treated as regex strings for backward compatibility.
|
|
20
|
+
*/
|
|
21
|
+
export function isGlobPattern(pattern) {
|
|
22
|
+
return /[*?[\]{]/.test(pattern);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Converts a glob pattern to a RegExp.
|
|
26
|
+
* Supports: ** (any path), * (any segment), ? (single char), {a,b} (alternation), [abc] (char class).
|
|
27
|
+
*/
|
|
28
|
+
export function globToRegex(glob) {
|
|
29
|
+
let src = "";
|
|
30
|
+
let i = 0;
|
|
31
|
+
while (i < glob.length) {
|
|
32
|
+
const c = glob[i];
|
|
33
|
+
if (c === "*") {
|
|
34
|
+
if (glob[i + 1] === "*") {
|
|
35
|
+
// ** matches anything including path separators
|
|
36
|
+
src += ".*";
|
|
37
|
+
i += 2;
|
|
38
|
+
// consume optional trailing slash so "fixtures/**" works
|
|
39
|
+
if (glob[i] === "/")
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// * matches anything within a single path segment
|
|
44
|
+
src += "[^/]*";
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (c === "?") {
|
|
49
|
+
src += "[^/]";
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
else if (c === "[") {
|
|
53
|
+
// Pass character classes through as-is
|
|
54
|
+
const close = glob.indexOf("]", i);
|
|
55
|
+
if (close !== -1) {
|
|
56
|
+
src += glob.slice(i, close + 1);
|
|
57
|
+
i = close + 1;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
src += "\\[";
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (c === "{") {
|
|
65
|
+
// {a,b,c} → (a|b|c)
|
|
66
|
+
const close = glob.indexOf("}", i);
|
|
67
|
+
if (close !== -1) {
|
|
68
|
+
const parts = glob
|
|
69
|
+
.slice(i + 1, close)
|
|
70
|
+
.split(",")
|
|
71
|
+
.map((p) => p.replace(/[.+^${}()|[\]\\]/g, "\\$&"));
|
|
72
|
+
src += `(${parts.join("|")})`;
|
|
73
|
+
i = close + 1;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
src += "\\{";
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (/[.+^$|()\\]/.test(c)) {
|
|
81
|
+
// Escape regex special characters
|
|
82
|
+
src += "\\" + c;
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
src += c;
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return new RegExp(src);
|
|
91
|
+
}
|
|
92
|
+
export function safeRegExp(source) {
|
|
93
|
+
try {
|
|
94
|
+
return new RegExp(source);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
throw new Error(`Invalid regex "${source}": ${msg}`);
|
|
99
|
+
}
|
|
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
|
+
}
|
|
17
114
|
/**
|
|
18
115
|
* SameValueZero-ish semantics:
|
|
19
116
|
* - NaN equals NaN
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadConfig.d.ts","sourceRoot":"","sources":["../../../src/utils/loadConfig.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"loadConfig.d.ts","sourceRoot":"","sources":["../../../src/utils/loadConfig.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAU1D,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,mBAAmB,CAAC;IAE5B,UAAU,EAAE,MAAM,CAAC;IAEnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAuCF,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,CAAC,CAevB;AAqCD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAc9B"}
|