@unrift/core 0.0.0 → 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.
Files changed (51) hide show
  1. package/dist/esm/cli.js +75 -5
  2. package/dist/esm/expect.d.ts +8 -0
  3. package/dist/esm/expect.d.ts.map +1 -1
  4. package/dist/esm/expect.js +48 -4
  5. package/dist/esm/index.d.ts +2 -2
  6. package/dist/esm/index.d.ts.map +1 -1
  7. package/dist/esm/index.js +1 -1
  8. package/dist/esm/interface.d.ts +4 -1
  9. package/dist/esm/interface.d.ts.map +1 -1
  10. package/dist/esm/interface.js +4 -1
  11. package/dist/esm/matchers.d.ts +1 -0
  12. package/dist/esm/matchers.d.ts.map +1 -1
  13. package/dist/esm/matchers.js +61 -0
  14. package/dist/esm/run.d.ts.map +1 -1
  15. package/dist/esm/run.js +3 -4
  16. package/dist/esm/runner.d.ts.map +1 -1
  17. package/dist/esm/runner.js +201 -33
  18. package/dist/esm/suite.d.ts +2 -10
  19. package/dist/esm/suite.d.ts.map +1 -1
  20. package/dist/esm/suite.js +29 -10
  21. package/dist/esm/test.d.ts +1 -7
  22. package/dist/esm/test.d.ts.map +1 -1
  23. package/dist/esm/test.js +11 -2
  24. package/dist/esm/types.d.ts +3 -3
  25. package/dist/esm/types.d.ts.map +1 -1
  26. package/dist/esm/types.js +1 -0
  27. package/dist/esm/utils/colors.d.ts +21 -0
  28. package/dist/esm/utils/colors.d.ts.map +1 -1
  29. package/dist/esm/utils/colors.js +34 -4
  30. package/dist/esm/utils/compareEngine.d.ts +10 -0
  31. package/dist/esm/utils/compareEngine.d.ts.map +1 -0
  32. package/dist/esm/utils/compareEngine.js +222 -0
  33. package/dist/esm/utils/deepEqual.d.ts.map +1 -1
  34. package/dist/esm/utils/deepEqual.js +7 -185
  35. package/dist/esm/utils/diff.d.ts +2 -0
  36. package/dist/esm/utils/diff.d.ts.map +1 -0
  37. package/dist/esm/utils/diff.js +84 -0
  38. package/dist/esm/utils/discoverTestFiles.d.ts.map +1 -1
  39. package/dist/esm/utils/discoverTestFiles.js +17 -2
  40. package/dist/esm/utils/looseEqual.d.ts +9 -0
  41. package/dist/esm/utils/looseEqual.d.ts.map +1 -1
  42. package/dist/esm/utils/looseEqual.js +16 -197
  43. package/dist/esm/utils/normalizePath.d.ts +2 -0
  44. package/dist/esm/utils/normalizePath.d.ts.map +1 -0
  45. package/dist/esm/utils/normalizePath.js +4 -0
  46. package/dist/esm/utils/toError.d.ts +2 -0
  47. package/dist/esm/utils/toError.d.ts.map +1 -0
  48. package/dist/esm/utils/toError.js +3 -0
  49. package/dist/esm/utils/transform.d.ts.map +1 -1
  50. package/dist/esm/utils/transform.js +31 -21
  51. package/package.json +21 -3
@@ -4,11 +4,8 @@ import { cleanUnriftCaches } from "./utils/transform.js";
4
4
  import { colors } from "./utils/colors.js";
5
5
  import { discoverTestFiles } from "./utils/discoverTestFiles.js";
6
6
  import { runEngine } from "./run.js";
7
+ import { normalizePath } from "./utils/normalizePath.js";
7
8
  import { TaskStatus } from "./types.js";
8
- function normalizePath(p) {
9
- // stable for matching/reporting across platforms
10
- return p.replace(/\\/g, "/");
11
- }
12
9
  function debugLog(enabled, json, ...args) {
13
10
  // Keep stdout clean for --json mode; use stderr for debug.
14
11
  if (!enabled)
@@ -24,6 +21,15 @@ function safeRegExp(source) {
24
21
  throw new Error(`Invalid regex "${source}": ${msg}`);
25
22
  }
26
23
  }
24
+ const regExpCache = new Map();
25
+ function getCachedRegExp(pattern) {
26
+ let re = regExpCache.get(pattern);
27
+ if (!re) {
28
+ re = safeRegExp(pattern);
29
+ regExpCache.set(pattern, re);
30
+ }
31
+ return re;
32
+ }
27
33
  /**
28
34
  * @TODO Add glob support
29
35
  */
@@ -31,7 +37,7 @@ function matchesAnyPattern(fileAbs, patterns, baseDir) {
31
37
  const absolutePath = normalizePath(fileAbs);
32
38
  const relativePath = normalizePath(relative(baseDir, fileAbs));
33
39
  return patterns.some((pattern) => {
34
- const re = safeRegExp(pattern);
40
+ const re = getCachedRegExp(pattern);
35
41
  return re.test(relativePath) || re.test(absolutePath);
36
42
  });
37
43
  }
@@ -50,15 +56,76 @@ function formatDuration(ms) {
50
56
  return `${ms.toFixed(0)}ms`;
51
57
  return `${(ms / 1000).toFixed(2)}s`;
52
58
  }
53
- function padRight(str, width) {
54
- return str.length >= width ? str : str + " ".repeat(width - str.length);
55
- }
56
59
  function getFileHeadingFromDescription(description) {
57
60
  const index = description.indexOf("›");
58
61
  if (index === -1)
59
62
  return null;
60
63
  return description.slice(0, index).trim();
61
64
  }
65
+ function shortenPath(absPath) {
66
+ const cwd = normalizePath(process.cwd());
67
+ const norm = normalizePath(absPath);
68
+ if (norm.startsWith(cwd + "/"))
69
+ return norm.slice(cwd.length + 1);
70
+ return norm;
71
+ }
72
+ function horizontalRule(label) {
73
+ const width = Math.min(process.stdout.columns || 80, 80);
74
+ if (!label)
75
+ return colors.dim("─".repeat(width));
76
+ const padding = 2;
77
+ const labelLen = label.length + padding * 2;
78
+ const sideLen = Math.max(1, Math.floor((width - labelLen) / 2));
79
+ const left = "─".repeat(sideLen);
80
+ const right = "─".repeat(Math.max(1, width - sideLen - labelLen));
81
+ return (colors.dim(`${left}${"─".repeat(padding)}`) +
82
+ label +
83
+ colors.dim(`${"─".repeat(padding)}${right}`));
84
+ }
85
+ function groupResultsByFile(results) {
86
+ const groups = [];
87
+ let current = null;
88
+ for (const result of results) {
89
+ const heading = getFileHeadingFromDescription(result.description);
90
+ const key = heading ?? "";
91
+ if (!current || current.heading !== key) {
92
+ current = { heading: key, results: [] };
93
+ groups.push(current);
94
+ }
95
+ current.results.push(result);
96
+ }
97
+ return groups;
98
+ }
99
+ function getFileStats(group) {
100
+ let pass = 0;
101
+ let fail = 0;
102
+ let skip = 0;
103
+ let todoCount = 0;
104
+ let totalDuration = 0;
105
+ for (const r of group.results) {
106
+ if (r.status === TaskStatus.Pass)
107
+ pass++;
108
+ else if (r.status === TaskStatus.Fail)
109
+ fail++;
110
+ else if (r.status === TaskStatus.Skipped)
111
+ skip++;
112
+ else if (r.status === TaskStatus.Todo)
113
+ todoCount++;
114
+ totalDuration += r.durationMs;
115
+ }
116
+ return {
117
+ pass,
118
+ fail,
119
+ skip,
120
+ todo: todoCount,
121
+ total: group.results.length,
122
+ totalDuration,
123
+ };
124
+ }
125
+ function extractTestName(description) {
126
+ // Remove the file heading and first suite separator, keep nested describe structure
127
+ return description.replace(/^.*?›\s*/, "");
128
+ }
62
129
  export async function runTestsCLI(options = {}) {
63
130
  const runStart = performance.now();
64
131
  // Cache clean mode
@@ -113,6 +180,33 @@ export async function runTestsCLI(options = {}) {
113
180
  }
114
181
  return;
115
182
  }
183
+ // No test files found
184
+ if (files.length === 0) {
185
+ const dir = normalizePath(testDir);
186
+ if (options.json) {
187
+ const report = {
188
+ ok: false,
189
+ failed: 0,
190
+ passed: 0,
191
+ skipped: 0,
192
+ todo: 0,
193
+ total: 0,
194
+ bail: options.bail ?? config?.bail ?? false,
195
+ timeoutMs: options.timeoutMs ?? config?.timeoutMs,
196
+ testDir: dir,
197
+ configPath: resolvedConfigPath,
198
+ files: [],
199
+ durationMs: performance.now() - runStart,
200
+ results: [],
201
+ };
202
+ console.log(JSON.stringify(report, null, 2));
203
+ }
204
+ else {
205
+ console.log(`\n ${colors.boldYellow("!")} No test files found in ${colors.bold(dir)}\n`);
206
+ }
207
+ process.exitCode = 1;
208
+ return;
209
+ }
116
210
  const timeoutMs = options.timeoutMs ?? config?.timeoutMs;
117
211
  const bail = options.bail ?? config?.bail ?? false;
118
212
  const engine = await runEngine({
@@ -127,6 +221,7 @@ export async function runTestsCLI(options = {}) {
127
221
  let passed = 0;
128
222
  let failed = 0;
129
223
  let skipped = 0;
224
+ let todo = 0;
130
225
  const failures = [];
131
226
  const serializedResults = [];
132
227
  for (const result of results) {
@@ -137,6 +232,8 @@ export async function runTestsCLI(options = {}) {
137
232
  skipped++;
138
233
  else if (result.status === TaskStatus.Fail)
139
234
  failed++;
235
+ else if (result.status === TaskStatus.Todo)
236
+ todo++;
140
237
  if (result.status === TaskStatus.Fail)
141
238
  failures.push(result);
142
239
  serializedResults.push({
@@ -157,6 +254,7 @@ export async function runTestsCLI(options = {}) {
157
254
  failed,
158
255
  passed,
159
256
  skipped,
257
+ todo,
160
258
  total,
161
259
  bail,
162
260
  timeoutMs,
@@ -171,37 +269,107 @@ export async function runTestsCLI(options = {}) {
171
269
  process.exitCode = 1;
172
270
  return;
173
271
  }
174
- // Standard reporting
175
- const durationColWidth = 8;
176
- let lastHeading = null;
177
- for (const result of results) {
178
- const heading = getFileHeadingFromDescription(result.description);
179
- if (heading && heading !== lastHeading) {
180
- console.log(colors.bold(`\n${heading}`));
181
- lastHeading = heading;
182
- }
183
- const duration = formatDuration(result.durationMs ?? 0);
184
- const durationPadded = padRight(duration, durationColWidth);
185
- if (result.status === TaskStatus.Pass) {
186
- console.log(colors.green("✔"), durationPadded, result.description.replace(/^.*?›\s*/, ""));
187
- continue;
188
- }
189
- if (result.status === TaskStatus.Skipped) {
190
- console.log(colors.yellow("↷"), padRight("—", durationColWidth), result.description.replace(/^.*?›\s*/, ""));
191
- continue;
272
+ // ── Pretty reporting ──────────────────────────────────────────────
273
+ const groups = groupResultsByFile(results);
274
+ // File-by-file results
275
+ for (const group of groups) {
276
+ const stats = getFileStats(group);
277
+ const shortPath = shortenPath(group.heading);
278
+ // File heading with inline summary
279
+ const fileIcon = stats.fail > 0 ? colors.boldRed("✘") : colors.boldGreen("✓");
280
+ const countParts = [];
281
+ if (stats.pass > 0)
282
+ countParts.push(colors.green(`${stats.pass} passed`));
283
+ if (stats.fail > 0)
284
+ countParts.push(colors.red(`${stats.fail} failed`));
285
+ if (stats.skip > 0)
286
+ countParts.push(colors.yellow(`${stats.skip} skipped`));
287
+ if (stats.todo > 0)
288
+ countParts.push(colors.magenta(`${stats.todo} todo`));
289
+ const fileDuration = colors.dim(formatDuration(stats.totalDuration));
290
+ const countSummary = colors.dim("(") + countParts.join(colors.dim(", ")) + colors.dim(")");
291
+ console.log(`\n ${fileIcon} ${colors.bold(shortPath)} ${countSummary} ${fileDuration}`);
292
+ // Individual test results, indented
293
+ let lastSuitePath = "";
294
+ for (const result of group.results) {
295
+ const testName = extractTestName(result.description);
296
+ const parts = testName.split(" › ");
297
+ const leaf = parts[parts.length - 1];
298
+ const suitePath = parts.slice(0, -1).join(" › ");
299
+ // Print suite name when it changes
300
+ if (suitePath && suitePath !== lastSuitePath) {
301
+ const depth = parts.length - 1;
302
+ const suiteIndent = " " + " ".repeat(Math.max(0, depth - 1));
303
+ console.log(`${suiteIndent}${colors.dim("›")} ${colors.bold(parts[depth - 1])}`);
304
+ lastSuitePath = suitePath;
305
+ }
306
+ const depth = parts.length - 1;
307
+ const indent = " " + " ".repeat(depth);
308
+ if (result.status === TaskStatus.Pass) {
309
+ const dur = colors.dim(formatDuration(result.durationMs ?? 0));
310
+ console.log(`${indent}${colors.green("✔")} ${colors.dim(leaf)} ${dur}`);
311
+ continue;
312
+ }
313
+ if (result.status === TaskStatus.Fail) {
314
+ const dur = colors.dim(formatDuration(result.durationMs ?? 0));
315
+ console.log(`${indent}${colors.red("✘")} ${colors.boldRed(leaf)} ${dur}`);
316
+ continue;
317
+ }
318
+ if (result.status === TaskStatus.Skipped) {
319
+ console.log(`${indent}${colors.yellow("↷")} ${colors.dim(leaf)}`);
320
+ continue;
321
+ }
322
+ if (result.status === TaskStatus.Todo) {
323
+ console.log(`${indent}${colors.magenta("○")} ${colors.dim(leaf)}`);
324
+ continue;
325
+ }
192
326
  }
193
- // Fail
194
- console.log(colors.red("✘"), durationPadded, result.description.replace(/^.*?›\s*/, ""));
195
327
  }
328
+ // ── Failure details ────────────────────────────────────────────────
196
329
  if (failures.length > 0) {
197
- console.log(colors.bold("\nFailures:"));
330
+ console.log(`\n${horizontalRule(colors.boldRed(" FAILURES "))}\n`);
198
331
  failures.forEach((r, i) => {
199
- console.log(colors.red(`\n${i + 1}) ${r.description}`));
200
- if (r.error)
201
- console.log(colors.red(r.error.stack ?? r.error.message));
332
+ const testName = extractTestName(r.description);
333
+ console.log(colors.boldRed(` ${i + 1}) ${testName}`));
334
+ if (r.error) {
335
+ const errText = r.error.stack ?? r.error.message;
336
+ const lines = errText.split("\n");
337
+ for (const line of lines) {
338
+ // Highlight the "Received/Expected" lines differently
339
+ if (line.trimStart().startsWith("Received:")) {
340
+ console.log(colors.red(` ${line.trim()}`));
341
+ }
342
+ else if (line.trimStart().startsWith("Expected:")) {
343
+ console.log(colors.green(` ${line.trim()}`));
344
+ }
345
+ else if (line.trim().startsWith("at ")) {
346
+ console.log(colors.dim(` ${line.trim()}`));
347
+ }
348
+ else {
349
+ console.log(` ${colors.red(line.trim())}`);
350
+ }
351
+ }
352
+ }
353
+ if (i < failures.length - 1)
354
+ console.log("");
202
355
  });
356
+ console.log(`\n${horizontalRule()}`);
203
357
  }
204
- console.log(colors.bold(`\nPassed: ${passed} Failed: ${failed} Skipped: ${skipped} Total: ${total} Time: ${formatDuration(durationMs)}\n`));
358
+ // ── Summary ────────────────────────────────────────────────────────
359
+ console.log("");
360
+ const summaryParts = [];
361
+ if (passed > 0)
362
+ summaryParts.push(colors.boldGreen(`${passed} passed`));
363
+ if (failed > 0)
364
+ summaryParts.push(colors.boldRed(`${failed} failed`));
365
+ if (skipped > 0)
366
+ summaryParts.push(colors.boldYellow(`${skipped} skipped`));
367
+ if (todo > 0)
368
+ summaryParts.push(colors.magenta(`${todo} todo`));
369
+ const badge = ok ? colors.badgePass(" PASS ") : colors.badgeFail(" FAIL ");
370
+ console.log(` ${badge} ${summaryParts.join(colors.dim(" · "))} ${colors.dim("of")} ${colors.bold(String(total))} ${colors.dim("tests")} ${colors.dim("in")} ${colors.bold(formatDuration(durationMs))}`);
371
+ console.log(` ${colors.dim(`${files.length} test file${files.length === 1 ? "" : "s"}`)}${bail ? colors.dim(" · bail mode") : ""}`);
372
+ console.log("");
205
373
  if (!ok)
206
374
  process.exitCode = 1;
207
375
  }
@@ -7,16 +7,7 @@ interface RunOptions {
7
7
  interface SuiteRunState {
8
8
  bailed: boolean;
9
9
  }
10
- interface SuiteTask {
11
- description: string;
12
- parent?: Suite;
13
- suites: Suite[];
14
- tests: Test[];
15
- addSuite(suite: Suite): void;
16
- addTest(test: Test): void;
17
- run(): Promise<void>;
18
- }
19
- export declare class Suite implements SuiteTask {
10
+ export declare class Suite {
20
11
  description: string;
21
12
  mode: TaskMode;
22
13
  parent?: Suite;
@@ -36,6 +27,7 @@ export declare class Suite implements SuiteTask {
36
27
  afterEachHooks: PromisableFn<void>[];
37
28
  constructor(description: string, parent?: Suite, mode?: TaskMode);
38
29
  markSubtreeSkipped(): void;
30
+ markSubtreeTodo(): void;
39
31
  addSuite(suite: Suite): void;
40
32
  addTest(test: Test): void;
41
33
  private collectBeforeEachHooks;
@@ -1 +1 @@
1
- {"version":3,"file":"suite.d.ts","sourceRoot":"","sources":["../../src/suite.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,OAAO,EAAE,KAAK,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAElE,UAAU,UAAU;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,UAAU,SAAS;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,KAAK,EAAE,IAAI,EAAE,CAAC;IAEd,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAE1B,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAsBD,qBAAa,KAAM,YAAW,SAAS;IACrC,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,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;IAwJhB,kBAAkB,IAAI,MAAM;IAK5B,KAAK;IAaL,UAAU,IAAI,KAAK,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,UAAU,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CAkDH;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAiBtD;AAED,eAAO,MAAM,SAAS,OAAoB,CAAC;AAG3C,wBAAgB,eAAe,UAE9B;AAED,wBAAgB,YAAY,SAI3B"}
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;AAGlE,UAAU,UAAU;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,OAAO,CAAC;CACjB;AAsBD,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;IASlB,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;IA4JhB,kBAAkB,IAAI,MAAM;IAK5B,KAAK;IAaL,UAAU,IAAI,KAAK,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,UAAU,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CAkDH;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,5 +1,7 @@
1
1
  import { unriftGlobalContext } from "./context.js";
2
+ import { resetMatchers } from "./matchers.js";
2
3
  import { TaskMode, TaskStatus } from "./types.js";
4
+ import { toError } from "./utils/toError.js";
3
5
  function appendHookFailure(existing, hookName, hookError) {
4
6
  if (!existing)
5
7
  return hookError;
@@ -40,6 +42,15 @@ export class Suite {
40
42
  for (const suite of this.suites)
41
43
  suite.markSubtreeSkipped();
42
44
  }
45
+ markSubtreeTodo() {
46
+ for (const test of this.tests) {
47
+ if (test.status === TaskStatus.Pending)
48
+ test.status = TaskStatus.Todo;
49
+ test.durationMs = 0;
50
+ }
51
+ for (const suite of this.suites)
52
+ suite.markSubtreeTodo();
53
+ }
43
54
  addSuite(suite) {
44
55
  this.suites.push(suite);
45
56
  }
@@ -78,6 +89,10 @@ export class Suite {
78
89
  this.markSubtreeSkipped();
79
90
  return;
80
91
  }
92
+ if (this.mode === TaskMode.Todo) {
93
+ this.markSubtreeTodo();
94
+ return;
95
+ }
81
96
  const suiteHasOnly = this.subtreeHasOnly;
82
97
  const inOnlyContext = ancestorOnly || this.mode === TaskMode.Only;
83
98
  // If only is on and this suite has no runnable-only content, skip it entirely
@@ -94,8 +109,7 @@ export class Suite {
94
109
  }
95
110
  }
96
111
  catch (error) {
97
- this.beforeAllError =
98
- error instanceof Error ? error : new Error(String(error));
112
+ this.beforeAllError = toError(error);
99
113
  // Prevent "pending" tests from showing up if beforeAll fails
100
114
  this.markSubtreeSkipped();
101
115
  return;
@@ -113,6 +127,11 @@ export class Suite {
113
127
  test.durationMs = 0;
114
128
  continue;
115
129
  }
130
+ if (test.mode === TaskMode.Todo) {
131
+ test.status = TaskStatus.Todo;
132
+ test.durationMs = 0;
133
+ continue;
134
+ }
116
135
  const testIsOnly = inOnlyContext || test.mode === TaskMode.Only;
117
136
  // If onlyMode, skip tests that are not in only context
118
137
  if (isOnly && !testIsOnly) {
@@ -126,16 +145,14 @@ export class Suite {
126
145
  await hook();
127
146
  }
128
147
  catch (error) {
129
- const hookError = error instanceof Error ? error : new Error(String(error));
130
- throw appendHookFailure(undefined, "beforeEach", hookError);
148
+ throw appendHookFailure(undefined, "beforeEach", toError(error));
131
149
  }
132
150
  }
133
151
  await test.run(options?.timeoutMs);
134
152
  }
135
153
  catch (error) {
136
154
  test.status = TaskStatus.Fail;
137
- test.error =
138
- error instanceof Error ? error : new Error(String(error));
155
+ test.error = toError(error);
139
156
  }
140
157
  finally {
141
158
  for (const hook of afterEachHooks) {
@@ -143,9 +160,8 @@ export class Suite {
143
160
  await hook();
144
161
  }
145
162
  catch (error) {
146
- const hookError = error instanceof Error ? error : new Error(String(error));
147
163
  test.status = TaskStatus.Fail;
148
- test.error = appendHookFailure(test.error, "afterEach", hookError);
164
+ test.error = appendHookFailure(test.error, "afterEach", toError(error));
149
165
  }
150
166
  }
151
167
  }
@@ -189,8 +205,7 @@ export class Suite {
189
205
  catch (error) {
190
206
  // Record once; keep running remaining afterAll hooks
191
207
  if (!this.afterAllError) {
192
- this.afterAllError =
193
- error instanceof Error ? error : new Error(String(error));
208
+ this.afterAllError = toError(error);
194
209
  }
195
210
  }
196
211
  }
@@ -269,6 +284,9 @@ export function computeOnlyFlags(suite) {
269
284
  suite.subtreeHasOnly = subtreeHasOnly;
270
285
  return subtreeHasOnly;
271
286
  }
287
+ // KNOWN LIMITATION: rootSuite is a global singleton. This means parallel test
288
+ // execution in the same process is not supported. A future DI/context-container
289
+ // pattern would be needed to support worker-based parallelism.
272
290
  export const rootSuite = new Suite("root");
273
291
  rootSuite.root = true;
274
292
  export function getCurrentSuite() {
@@ -277,5 +295,6 @@ export function getCurrentSuite() {
277
295
  export function clearContext() {
278
296
  rootSuite.reset();
279
297
  rootSuite.subtreeHasOnly = false;
298
+ resetMatchers();
280
299
  unriftGlobalContext.currentSuite = rootSuite;
281
300
  }
@@ -1,10 +1,5 @@
1
1
  import { TaskMode, TaskStatus, type PromisableFn } from "./types";
2
- interface TestTask {
3
- description: string;
4
- fn: PromisableFn<void>;
5
- run(): Promise<void>;
6
- }
7
- export declare class Test implements TestTask {
2
+ export declare class Test {
8
3
  description: string;
9
4
  durationMs: number;
10
5
  error?: Error;
@@ -14,5 +9,4 @@ export declare class Test implements TestTask {
14
9
  constructor(description: string, fn: PromisableFn<void>, mode?: TaskMode);
15
10
  run(timeoutMs?: number): Promise<void>;
16
11
  }
17
- export {};
18
12
  //# sourceMappingURL=test.d.ts.map
@@ -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;AAElE,UAAU,QAAQ;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IACvB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,qBAAa,IAAK,YAAW,QAAQ;IACnC,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;gBAGtC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE,QAA2B;IAO7B,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAwC7C"}
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;AAGlE,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;gBAGtC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE,QAA2B;IAO7B,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAsD7C"}
package/dist/esm/test.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { TaskMode, TaskStatus } from "./types.js";
2
+ import { toError } from "./utils/toError.js";
2
3
  export class Test {
3
4
  description;
4
5
  durationMs = 0;
@@ -17,6 +18,11 @@ export class Test {
17
18
  this.durationMs = 0;
18
19
  return;
19
20
  }
21
+ if (this.mode === TaskMode.Todo) {
22
+ this.status = TaskStatus.Todo;
23
+ this.durationMs = 0;
24
+ return;
25
+ }
20
26
  const start = performance.now();
21
27
  this.status = TaskStatus.Running;
22
28
  const exec = Promise.resolve().then(() => this.fn());
@@ -25,9 +31,12 @@ export class Test {
25
31
  let timeoutId = null;
26
32
  const timeoutFn = new Promise((_, reject) => {
27
33
  timeoutId = setTimeout(() => {
28
- reject(new Error(`Test timed out after ${timeoutMs} ms`));
34
+ reject(new Error(`Test "${this.description}" timed out after ${timeoutMs} ms`));
29
35
  }, timeoutMs);
30
36
  });
37
+ // NOTE: Promise.race does not cancel the losing promise. A timed-out test
38
+ // function continues running in the background. True cancellation would
39
+ // require AbortController integration, which is a larger future change.
31
40
  try {
32
41
  await Promise.race([exec, timeoutFn]);
33
42
  }
@@ -42,7 +51,7 @@ export class Test {
42
51
  this.status = TaskStatus.Pass;
43
52
  }
44
53
  catch (error) {
45
- this.error = error instanceof Error ? error : new Error(String(error));
54
+ this.error = toError(error);
46
55
  this.status = TaskStatus.Fail;
47
56
  }
48
57
  finally {
@@ -1,4 +1,4 @@
1
- type Promisable<T> = Promise<T> | T;
1
+ export type Promisable<T> = Promise<T> | T;
2
2
  export type PromisableFn<T> = () => Promisable<T>;
3
3
  export declare enum TaskStatus {
4
4
  Pending = "pending",
@@ -12,7 +12,8 @@ export declare enum TaskStatus {
12
12
  export declare enum TaskMode {
13
13
  Default = "default",
14
14
  Only = "only",
15
- Skip = "skip"
15
+ Skip = "skip",
16
+ Todo = "todo"
16
17
  }
17
18
  export interface MatcherContext {
18
19
  isNot: boolean;
@@ -20,5 +21,4 @@ export interface MatcherContext {
20
21
  }
21
22
  export type MatcherFn = (this: MatcherContext, received: unknown, ...args: readonly unknown[]) => void | Promise<void>;
22
23
  export type MatcherMap = Record<string, MatcherFn>;
23
- export {};
24
24
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAEpC,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,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,IAAI,SAAS;CACd;AAED,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,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"}
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,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
@@ -13,4 +13,5 @@ export var TaskMode;
13
13
  TaskMode["Default"] = "default";
14
14
  TaskMode["Only"] = "only";
15
15
  TaskMode["Skip"] = "skip";
16
+ TaskMode["Todo"] = "todo";
16
17
  })(TaskMode || (TaskMode = {}));
@@ -2,6 +2,27 @@ export declare const colors: {
2
2
  green: (text: string) => string;
3
3
  red: (text: string) => string;
4
4
  yellow: (text: string) => string;
5
+ cyan: (text: string) => string;
6
+ magenta: (text: string) => string;
7
+ white: (text: string) => string;
5
8
  bold: (text: string) => string;
9
+ dim: (text: string) => string;
10
+ italic: (text: string) => string;
11
+ underline: (text: string) => string;
12
+ strikethrough: (text: string) => string;
13
+ gray: (text: string) => string;
14
+ bgRed: (text: string) => string;
15
+ bgGreen: (text: string) => string;
16
+ bgYellow: (text: string) => string;
17
+ bgCyan: (text: string) => string;
18
+ boldGreen: (text: string) => string;
19
+ boldRed: (text: string) => string;
20
+ boldYellow: (text: string) => string;
21
+ boldCyan: (text: string) => string;
22
+ boldDim: (text: string) => string;
23
+ badgePass: (text: string) => string;
24
+ badgeFail: (text: string) => string;
25
+ badgeSkip: (text: string) => string;
26
+ badgeTodo: (text: string) => string;
6
27
  };
7
28
  //# sourceMappingURL=colors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../../../src/utils/colors.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,MAAM;kBAHH,MAAM;gBAAN,MAAM;mBAAN,MAAM;iBAAN,MAAM;CAQrB,CAAC"}
1
+ {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../../../src/utils/colors.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,MAAM;kBARH,MAAM;gBAAN,MAAM;mBAAN,MAAM;iBAAN,MAAM;oBAAN,MAAM;kBAAN,MAAM;iBAAN,MAAM;gBAAN,MAAM;mBAAN,MAAM;sBAAN,MAAM;0BAAN,MAAM;iBAAN,MAAM;kBAAN,MAAM;oBAAN,MAAM;qBAAN,MAAM;mBAAN,MAAM;sBAIN,MAAM;oBAAN,MAAM;uBAAN,MAAM;qBAAN,MAAM;oBAAN,MAAM;sBAAN,MAAM;sBAAN,MAAM;sBAAN,MAAM;sBAAN,MAAM;CAyCrB,CAAC"}
@@ -4,9 +4,39 @@ const enabled = process.stdout.isTTY &&
4
4
  function wrap(code) {
5
5
  return (text) => (enabled ? `\x1b[${code}m${text}\x1b[0m` : text);
6
6
  }
7
+ function compose(...codes) {
8
+ return (text) => enabled ? `\x1b[${codes.join(";")}m${text}\x1b[0m` : text;
9
+ }
7
10
  export const colors = {
8
- green: wrap(32),
9
- red: wrap(31),
10
- yellow: wrap(33),
11
- bold: wrap(1),
11
+ // Basic colors
12
+ green: wrap("32"),
13
+ red: wrap("31"),
14
+ yellow: wrap("33"),
15
+ cyan: wrap("36"),
16
+ magenta: wrap("35"),
17
+ white: wrap("37"),
18
+ // Modifiers
19
+ bold: wrap("1"),
20
+ dim: wrap("2"),
21
+ italic: wrap("3"),
22
+ underline: wrap("4"),
23
+ strikethrough: wrap("9"),
24
+ // Bright colors
25
+ gray: wrap("90"),
26
+ // Backgrounds
27
+ bgRed: wrap("41"),
28
+ bgGreen: wrap("42"),
29
+ bgYellow: wrap("43"),
30
+ bgCyan: wrap("46"),
31
+ // Composites
32
+ boldGreen: compose("1", "32"),
33
+ boldRed: compose("1", "31"),
34
+ boldYellow: compose("1", "33"),
35
+ boldCyan: compose("1", "36"),
36
+ boldDim: compose("1", "2"),
37
+ // Badge-style (white on colored background)
38
+ badgePass: compose("1", "30", "42"),
39
+ badgeFail: compose("1", "37", "41"),
40
+ badgeSkip: compose("1", "30", "43"),
41
+ badgeTodo: compose("1", "35"),
12
42
  };
@@ -0,0 +1,10 @@
1
+ export interface CompareOptions {
2
+ /** true for deepEqual (prototype-sensitive), false for looseEqual */
3
+ checkPrototype: boolean;
4
+ /** true for looseEqual (sparse holes → undefined), false for deepEqual */
5
+ treatSparseAsUndefined: boolean;
6
+ /** true for looseEqual (bidirectional cycle tracking) */
7
+ bijectiveCycleCheck: boolean;
8
+ }
9
+ export declare function compareInternal(left: unknown, right: unknown, options: CompareOptions, leftToRightSeen: WeakMap<object, object>, rightToLeftSeen?: WeakMap<object, object>): boolean;
10
+ //# sourceMappingURL=compareEngine.d.ts.map
@@ -0,0 +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,CA6RT"}