@unrift/core 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/cli.js CHANGED
@@ -36,6 +36,7 @@ Usage:
36
36
 
37
37
  Options:
38
38
  -c, --config <path> Path to config file
39
+ -g, --grep <pattern> Only run tests whose full name matches (regex)
39
40
  -b, --bail Stop on first test failure
40
41
  -t, --timeout <ms> Default test timeout in milliseconds
41
42
  -w, --watch Re-run tests on file changes
@@ -49,6 +50,7 @@ Options:
49
50
  Examples:
50
51
  unrift
51
52
  unrift math
53
+ unrift --grep "adds numbers"
52
54
  unrift -c test/config.ts --bail
53
55
  unrift --timeout 10000
54
56
  unrift --watch
@@ -57,6 +59,7 @@ Examples:
57
59
  function parseArgs(argv) {
58
60
  let configPath;
59
61
  let pattern;
62
+ let grep;
60
63
  let timeoutMs;
61
64
  let bail;
62
65
  let debug = false;
@@ -85,6 +88,14 @@ function parseArgs(argv) {
85
88
  i++;
86
89
  continue;
87
90
  }
91
+ if (a === "--grep" || a === "-g") {
92
+ const next = argv[i + 1];
93
+ if (!next)
94
+ throw new Error(`${a} requires a pattern`);
95
+ grep = safeRegExp(next);
96
+ i++;
97
+ continue;
98
+ }
88
99
  if (a === "--bail" || a === "-b") {
89
100
  bail = true;
90
101
  continue;
@@ -127,6 +138,7 @@ function parseArgs(argv) {
127
138
  return {
128
139
  configPath,
129
140
  pattern,
141
+ grep,
130
142
  timeoutMs,
131
143
  bail,
132
144
  debug,
@@ -138,7 +150,7 @@ function parseArgs(argv) {
138
150
  showVersion,
139
151
  };
140
152
  }
141
- const { configPath, pattern, timeoutMs, bail, debug, list, json, cacheClean, watch, help, showVersion, } = parseArgs(process.argv.slice(2));
153
+ const { configPath, pattern, grep, timeoutMs, bail, debug, list, json, cacheClean, watch, help, showVersion, } = parseArgs(process.argv.slice(2));
142
154
  if (showVersion) {
143
155
  console.log(version);
144
156
  process.exit(0);
@@ -149,6 +161,7 @@ if (help) {
149
161
  }
150
162
  const runOpts = {
151
163
  pattern,
164
+ grep,
152
165
  configPath,
153
166
  timeoutMs,
154
167
  bail,
@@ -17,6 +17,7 @@ export interface Matchers<T> {
17
17
  prototype: unknown;
18
18
  }): void | Promise<void>;
19
19
  toContain(expected: unknown): void | Promise<void>;
20
+ toContainEqual(expected: unknown): void | Promise<void>;
20
21
  toEqual(expected: unknown): void | Promise<void>;
21
22
  toHaveLength(expected: number): void | Promise<void>;
22
23
  toMatch(expected: RegExp | string): void | Promise<void>;
@@ -24,6 +25,7 @@ export interface Matchers<T> {
24
25
  toThrow(expected?: unknown): void | Promise<void>;
25
26
  toBeGreaterThanOrEqual(expected: number): void | Promise<void>;
26
27
  toBeLessThanOrEqual(expected: number): void | Promise<void>;
28
+ toBeCloseTo(expected: number, precision?: number): void | Promise<void>;
27
29
  toBeNaN(): void | Promise<void>;
28
30
  toBeFinite(): void | Promise<void>;
29
31
  toMatchObject(expected: Record<string, unknown>): void | Promise<void>;
@@ -41,7 +43,7 @@ export interface Matchers<T> {
41
43
  toHaveFetched(urlOrMatcher: string | RegExp | ((req: Request) => boolean)): void | Promise<void>;
42
44
  toHaveFetchedTimes(expected: number): void | Promise<void>;
43
45
  readonly not: Matchers<T>;
44
- readonly resolves: Matchers<Awaited<T>>;
46
+ readonly resolves: Matchers<Settled<T>>;
45
47
  readonly rejects: Matchers<unknown>;
46
48
  }
47
49
  export interface Matchers<T> {
@@ -51,5 +53,8 @@ export interface ExpectInterface {
51
53
  extend<M extends MatcherMap>(m: M): void;
52
54
  readonly matchers: Readonly<MatcherMap>;
53
55
  }
56
+ /** What `.resolves` sees: a function's returned promise, or the promise itself. */
57
+ type Settled<T> = Awaited<T extends (...args: never[]) => infer R ? R : T>;
54
58
  export declare const expect: ExpectInterface;
59
+ export {};
55
60
  //# sourceMappingURL=expect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"expect.d.ts","sourceRoot":"","sources":["../../src/expect.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"expect.d.ts","sourceRoot":"","sources":["../../src/expect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,UAAU,EAAE,MAAM,SAAS,CAAC;AAM1D,wBAAgB,cAAc,CAAC,WAAW,EAAE,UAAU,QAErD;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,WAAW,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,QAAQ,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,UAAU,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,aAAa,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,cAAc,CAAC,QAAQ,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,cAAc,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,aAAa,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,UAAU,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,cAAc,CACZ,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,KAAK,CAAC,EAAE,OAAO,GACd,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAGxB,gBAAgB,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,oBAAoB,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,wBAAwB,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,uBAAuB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,cAAc,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,kBAAkB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,sBAAsB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,qBAAqB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAG1E,aAAa,CACX,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,GAC1D,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3D,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;CACrC;AAID,MAAM,WAAW,QAAQ,CAAC,CAAC;CAAI;AAE/B,MAAM,WAAW,eAAe;IAC9B,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3B,MAAM,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;CACzC;AAED,mFAAmF;AACnF,KAAK,OAAO,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAc3E,eAAO,MAAM,MAAM,EAAE,eAiJjB,CAAC"}
@@ -1,14 +1,24 @@
1
1
  import { formatDiff } from "./utils/diff.js";
2
+ import { isErrorLike, safeString } from "./utils/helpers.js";
2
3
  const matcherRegistry = Object.create(null);
3
4
  export function extendMatchers(newMatchers) {
4
5
  for (const key in newMatchers)
5
6
  matcherRegistry[key] = newMatchers[key];
6
7
  }
8
+ function describeError(value) {
9
+ return isErrorLike(value) ? value.message : safeString(value);
10
+ }
11
+ function isThenable(value) {
12
+ return (value != null &&
13
+ (typeof value === "object" || typeof value === "function") &&
14
+ typeof value.then === "function");
15
+ }
7
16
  export const expect = (() => {
8
17
  function expectFn(received) {
9
18
  function makeExpectation(isNot) {
10
19
  const ctx = {
11
20
  isNot,
21
+ promise: "",
12
22
  diff(a, b) {
13
23
  return formatDiff(a, b);
14
24
  },
@@ -18,9 +28,10 @@ export const expect = (() => {
18
28
  const fn = matcherRegistry[name];
19
29
  matcherFns[name] = (...args) => fn.call(ctx, received, ...args);
20
30
  }
21
- function makeAsyncProxy(getActual, asyncIsNot) {
31
+ function makeAsyncProxy(getActual, asyncIsNot, promise) {
22
32
  const asyncCtx = {
23
33
  isNot: asyncIsNot,
34
+ promise,
24
35
  diff(a, b) {
25
36
  return formatDiff(a, b);
26
37
  },
@@ -28,15 +39,14 @@ export const expect = (() => {
28
39
  const asyncFns = {};
29
40
  for (const name in matcherRegistry) {
30
41
  const fn = matcherRegistry[name];
31
- asyncFns[name] = async (...args) => {
32
- const actual = await getActual();
33
- return fn.call(asyncCtx, actual, ...args);
34
- };
42
+ // Not async: getActual() runs now, so a bad received value (a sync
43
+ // throw, a non-promise) throws immediately like Jest, even unawaited.
44
+ asyncFns[name] = (...args) => getActual().then((actual) => fn.call(asyncCtx, actual, ...args));
35
45
  }
36
46
  return new Proxy(asyncFns, {
37
47
  get(target, prop) {
38
48
  if (prop === "not") {
39
- return makeAsyncProxy(getActual, !asyncIsNot);
49
+ return makeAsyncProxy(getActual, !asyncIsNot, promise);
40
50
  }
41
51
  if (typeof prop === "string" && !(prop in target)) {
42
52
  throw new Error(`Unknown matcher: ${prop}`);
@@ -49,30 +59,51 @@ export const expect = (() => {
49
59
  get(target, prop) {
50
60
  if (prop === "not")
51
61
  return makeExpectation(!isNot);
62
+ // Same as Jest: a function is called and must return a promise. A
63
+ // sync throw from it isn't a rejection, it fails the assertion.
64
+ const toPromise = (modifier) => {
65
+ const value = typeof received === "function"
66
+ ? received()
67
+ : received;
68
+ if (!isThenable(value)) {
69
+ throw new TypeError(`.${modifier}: received value must be a promise or a function returning a promise, got ${describeError(value)}`);
70
+ }
71
+ return value;
72
+ };
52
73
  if (prop === "resolves") {
53
- return makeAsyncProxy(async () => {
54
- try {
55
- return await received;
56
- }
57
- catch (err) {
58
- throw new Error(`Expected promise to resolve, but it rejected with: ${err instanceof Error ? err.message : String(err)}`);
59
- }
60
- }, isNot);
74
+ return makeAsyncProxy(() => {
75
+ const pending = toPromise("resolves");
76
+ return (async () => {
77
+ try {
78
+ return await pending;
79
+ }
80
+ catch (err) {
81
+ throw new Error(`Expected promise to resolve, but it rejected with: ${describeError(err)}`);
82
+ }
83
+ })();
84
+ }, isNot, "resolves");
61
85
  }
62
86
  if (prop === "rejects") {
63
- return makeAsyncProxy(async () => {
64
- try {
65
- await received;
66
- throw new Error("Expected promise to reject, but it resolved");
67
- }
68
- catch (err) {
69
- if (err instanceof Error &&
70
- err.message === "Expected promise to reject, but it resolved") {
71
- throw err;
87
+ return makeAsyncProxy(() => {
88
+ const pending = toPromise("rejects");
89
+ return (async () => {
90
+ // Tracked with a flag, not by error message, so a promise that
91
+ // rejects with that same text is still treated as a rejection.
92
+ let resolved = false;
93
+ let reason;
94
+ try {
95
+ await pending;
96
+ resolved = true;
97
+ }
98
+ catch (err) {
99
+ reason = err;
100
+ }
101
+ if (resolved) {
102
+ throw new Error("Expected promise to reject, but it resolved");
72
103
  }
73
- return err;
74
- }
75
- }, isNot);
104
+ return reason;
105
+ })();
106
+ }, isNot, "rejects");
76
107
  }
77
108
  if (typeof prop === "string" && !(prop in target)) {
78
109
  throw new Error(`Unknown matcher: ${prop}`);
@@ -1,6 +1,6 @@
1
1
  export { beforeEach, afterEach, beforeAll, afterAll } from "./hooks";
2
2
  export { defineConfig, type UnriftConfigOptions } from "./utils/defineConfig";
3
- export { describe, it, test } from "./interface";
3
+ export { describe, it, test, type TestOptions } from "./interface";
4
4
  export { expect, extendMatchers } from "./expect";
5
5
  export type { Matchers, ExpectInterface } from "./expect";
6
6
  export type { MatcherContext, MatcherFn, MatcherMap, Promisable, } from "./types";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAElD,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC1D,YAAY,EACV,cAAc,EACd,SAAS,EACT,UAAU,EACV,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,YAAY,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAG7C,OAAO,EACL,IAAI,EACJ,GAAG,EACH,KAAK,EACL,KAAK,EACL,UAAU,EACV,SAAS,EACT,IAAI,EACJ,UAAU,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,eAAe,EACf,UAAU,EACV,MAAM,EACN,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,GAAG,EACH,SAAS,EACT,UAAU,EACV,SAAS,EACT,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,WAAW,GACZ,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAElD,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC1D,YAAY,EACV,cAAc,EACd,SAAS,EACT,UAAU,EACV,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,YAAY,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAG7C,OAAO,EACL,IAAI,EACJ,GAAG,EACH,KAAK,EACL,KAAK,EACL,UAAU,EACV,SAAS,EACT,IAAI,EACJ,UAAU,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,eAAe,EACf,UAAU,EACV,MAAM,EACN,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,GAAG,EACH,SAAS,EACT,UAAU,EACV,SAAS,EACT,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,WAAW,GACZ,MAAM,cAAc,CAAC"}
@@ -8,10 +8,14 @@ type DescribeFn = {
8
8
  skip: (description: string, fn: () => void) => void;
9
9
  todo: (description: string) => void;
10
10
  };
11
+ export interface TestOptions {
12
+ timeout?: number;
13
+ retries?: number;
14
+ }
11
15
  type ItFn = {
12
- (description: string, fn: PromisableFn<void>, timeoutMs?: number): void;
13
- only: (description: string, fn: PromisableFn<void>, timeoutMs?: number) => void;
14
- skip: (description: string, fn: PromisableFn<void>, timeoutMs?: number) => void;
16
+ (description: string, fn: PromisableFn<void>, options?: number | TestOptions): void;
17
+ only: (description: string, fn: PromisableFn<void>, options?: number | TestOptions) => void;
18
+ skip: (description: string, fn: PromisableFn<void>, options?: number | TestOptions) => void;
15
19
  todo: (description: string) => void;
16
20
  };
17
21
  declare const describe: DescribeFn;
@@ -1 +1 @@
1
- {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/interface.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,YAAY,EAAY,MAAM,SAAS,CAAC;AAEtD;;GAEG;AAEH,KAAK,UAAU,GAAG;IAChB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC5C,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACpD,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACpD,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,KAAK,IAAI,GAAG;IACV,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxE,IAAI,EAAE,CACJ,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,SAAS,CAAC,EAAE,MAAM,KACf,IAAI,CAAC;IACV,IAAI,EAAE,CACJ,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,SAAS,CAAC,EAAE,MAAM,KACf,IAAI,CAAC;IACV,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AA6BF,QAAA,MAAM,QAAQ,EAAE,UAC8B,CAAC;AAmB/C,QAAA,MAAM,EAAE,EAAE,IACyC,CAAC;AAOpD,QAAA,MAAM,IAAI,EAAE,IAAS,CAAC;AAEtB,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC"}
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/interface.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,YAAY,EAAY,MAAM,SAAS,CAAC;AAGtD;;GAEG;AAEH,KAAK,UAAU,GAAG;IAChB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC5C,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACpD,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACpD,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,IAAI,GAAG;IACV,CACE,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,GAC7B,IAAI,CAAC;IACR,IAAI,EAAE,CACJ,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,KAC3B,IAAI,CAAC;IACV,IAAI,EAAE,CACJ,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,KAC3B,IAAI,CAAC;IACV,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AA6BF,QAAA,MAAM,QAAQ,EAAE,UAC8B,CAAC;AAyB/C,QAAA,MAAM,EAAE,EAAE,IACuC,CAAC;AAOlD,QAAA,MAAM,IAAI,EAAE,IAAS,CAAC;AAEtB,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC"}
@@ -2,6 +2,7 @@ import { assertRegisterState, unriftGlobalContext } from "./context.js";
2
2
  import { getCurrentSuite, Suite } from "./suite.js";
3
3
  import { Test } from "./test.js";
4
4
  import { TaskMode } from "./types.js";
5
+ import { assertValidRetries } from "./utils/helpers.js";
5
6
  function _describe(description, fn, mode) {
6
7
  assertRegisterState("describe");
7
8
  const parent = getCurrentSuite();
@@ -24,14 +25,17 @@ const describe = (description, fn) => _describe(description, fn, TaskMode.Defaul
24
25
  describe.only = (description, fn) => _describe(description, fn, TaskMode.Only);
25
26
  describe.skip = (description, fn) => _describe(description, fn, TaskMode.Skip);
26
27
  describe.todo = (description) => _describe(description, () => { }, TaskMode.Todo);
27
- function _it(description, fn, mode, timeoutMs) {
28
+ function _it(description, fn, mode, options) {
28
29
  assertRegisterState("it");
29
- const test = new Test(description, fn, mode, timeoutMs);
30
+ // A bare number is the timeout, kept for backwards compatibility.
31
+ const { timeout, retries } = typeof options === "number" ? { timeout: options } : (options ?? {});
32
+ assertValidRetries(retries, `it("${description}")`);
33
+ const test = new Test(description, fn, mode, timeout, retries);
30
34
  getCurrentSuite().addTest(test);
31
35
  }
32
- const it = (description, fn, timeoutMs) => _it(description, fn, TaskMode.Default, timeoutMs);
33
- it.only = (description, fn, timeoutMs) => _it(description, fn, TaskMode.Only, timeoutMs);
34
- it.skip = (description, fn, timeoutMs) => _it(description, fn, TaskMode.Skip, timeoutMs);
36
+ const it = (description, fn, options) => _it(description, fn, TaskMode.Default, options);
37
+ it.only = (description, fn, options) => _it(description, fn, TaskMode.Only, options);
38
+ it.skip = (description, fn, options) => _it(description, fn, TaskMode.Skip, options);
35
39
  it.todo = (description) => _it(description, () => { }, TaskMode.Todo);
36
40
  const test = it;
37
41
  export { describe, it, test };
@@ -1 +1 @@
1
- {"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../../src/matchers.ts"],"names":[],"mappings":"AAyGA,wBAAgB,sBAAsB,SAMrC;AAED,wBAAgB,aAAa,SAE5B"}
1
+ {"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../../src/matchers.ts"],"names":[],"mappings":"AA6GA,wBAAgB,sBAAsB,SAMrC;AAED,wBAAgB,aAAa,SAE5B"}
@@ -4,7 +4,7 @@ import { isFetchMock } from "./mock/fetch.js";
4
4
  import { deepEqual } from "./utils/deepEqual.js";
5
5
  import { looseEqual } from "./utils/looseEqual.js";
6
6
  import { regexTest } from "./utils/helpers.js";
7
- import { toThrow } from "./utils/toThrow.js";
7
+ import { toThrow, toThrowFromPromise, } from "./utils/toThrow.js";
8
8
  let matchersRegistered = false;
9
9
  function ensureSpy(received, matcherName) {
10
10
  if (!isSpy(received)) {
@@ -165,6 +165,22 @@ function registerCoreMatchers() {
165
165
  throw new Error(this.diff(received, expected));
166
166
  }
167
167
  },
168
+ toContainEqual(received, expected) {
169
+ if (!Array.isArray(received)) {
170
+ throw new Error(this.diff(received, "expected an array for toContainEqual()"));
171
+ }
172
+ // for...of visits sparse holes as undefined; Array.some would skip them.
173
+ let pass = false;
174
+ for (const item of received) {
175
+ if (looseEqual(item, expected)) {
176
+ pass = true;
177
+ break;
178
+ }
179
+ }
180
+ if (this.isNot ? pass : !pass) {
181
+ throw new Error(this.diff(received, expected));
182
+ }
183
+ },
168
184
  toEqual(received, expected) {
169
185
  const pass = looseEqual(received, expected);
170
186
  if (this.isNot ? pass : !pass) {
@@ -207,7 +223,10 @@ function registerCoreMatchers() {
207
223
  }
208
224
  },
209
225
  toThrow(received, expected) {
210
- const { pass, message } = toThrow(received, expected, this.isNot);
226
+ // Under .resolves/.rejects, `received` is the settled value itself.
227
+ const { pass, message } = this.promise
228
+ ? toThrowFromPromise(received, expected, this.isNot, this.promise)
229
+ : toThrow(received, expected, this.isNot);
211
230
  if (!pass)
212
231
  throw new Error(message());
213
232
  },
@@ -229,6 +248,24 @@ function registerCoreMatchers() {
229
248
  throw new Error(this.diff(received, `<= ${expected}`));
230
249
  }
231
250
  },
251
+ toBeCloseTo(received, expected, precision = 2) {
252
+ if (typeof received !== "number") {
253
+ throw new Error(this.diff(received, "expected a number"));
254
+ }
255
+ if (typeof expected !== "number") {
256
+ throw new Error(this.diff(expected, "a number for expected"));
257
+ }
258
+ if (!Number.isFinite(precision)) {
259
+ throw new Error(this.diff(precision, "a finite number for precision"));
260
+ }
261
+ // Same-signed infinities are equal; the subtraction below would be NaN.
262
+ // Finite values always go through the tolerance check.
263
+ const pass = (!Number.isFinite(received) && received === expected) ||
264
+ Math.abs(received - expected) < 10 ** -precision / 2;
265
+ if (this.isNot ? pass : !pass) {
266
+ throw new Error(this.diff(received, `${expected} (precision ${precision})`));
267
+ }
268
+ },
232
269
  toBeNaN(received) {
233
270
  const pass = Number.isNaN(received);
234
271
  if (this.isNot ? pass : !pass) {
package/dist/esm/run.d.ts CHANGED
@@ -1,20 +1,17 @@
1
- import { TaskStatus } from "./types";
1
+ import { type TestResult } from "./suite";
2
2
  type RunEngineOptions = {
3
3
  files: string[];
4
4
  timeoutMs?: number;
5
5
  bail?: boolean;
6
+ grep?: RegExp;
7
+ retries?: number;
6
8
  matchers?: string[];
7
9
  };
8
10
  export type RunEngineResult = {
9
11
  isOnly: boolean;
10
12
  timeoutMs?: number;
11
13
  bail: boolean;
12
- results: Array<{
13
- description: string;
14
- status: TaskStatus;
15
- error?: Error;
16
- durationMs: number;
17
- }>;
14
+ results: TestResult[];
18
15
  };
19
16
  export declare function runEngine(options: RunEngineOptions): Promise<RunEngineResult>;
20
17
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAAY,UAAU,EAAE,MAAM,SAAS,CAAC;AAI/C,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,KAAK,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,UAAU,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ,CAAC;AAQF,wBAAsB,SAAS,CAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC,CA2D1B"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,SAAS,CAAC;AASjB,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB,CAAC;AAQF,wBAAsB,SAAS,CAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC,CAoE1B"}
package/dist/esm/run.js CHANGED
@@ -1,10 +1,11 @@
1
- import { clearContext, computeOnlyFlags, rootSuite, Suite } from "./suite.js";
1
+ import { clearContext, computeOnlyFlags, rootSuite, Suite, } from "./suite.js";
2
2
  import { ensureInternalMatchers } from "./matchers.js";
3
3
  import { RunState, unriftGlobalContext } from "./context.js";
4
4
  import { normalizePath } from "./utils/normalizePath.js";
5
5
  import { TaskMode } from "./types.js";
6
6
  import { toError } from "./utils/toError.js";
7
7
  import { toImportUrl } from "./utils/transform.js";
8
+ import { assertValidRetries } from "./utils/helpers.js";
8
9
  async function loadMatcherModules(specifiers) {
9
10
  for (const spec of specifiers ?? []) {
10
11
  await import(spec);
@@ -12,6 +13,9 @@ async function loadMatcherModules(specifiers) {
12
13
  }
13
14
  export async function runEngine(options) {
14
15
  const { files } = options;
16
+ // Validate before touching global state. A bad value (e.g. "3" from a JSON
17
+ // config) would otherwise turn into "31" attempts or zero.
18
+ assertValidRetries(options.retries, "runEngine");
15
19
  clearContext();
16
20
  ensureInternalMatchers();
17
21
  await loadMatcherModules(options.matchers);
@@ -40,7 +44,7 @@ export async function runEngine(options) {
40
44
  }
41
45
  const isOnly = computeOnlyFlags(rootSuite);
42
46
  unriftGlobalContext.state = RunState.Run;
43
- await rootSuite.run({ timeoutMs, bail }, { bailed: false }, isOnly, false);
47
+ await rootSuite.run({ timeoutMs, bail, grep: options.grep, retries: options.retries }, { bailed: false }, isOnly, false);
44
48
  const results = rootSuite.getResults();
45
49
  return {
46
50
  isOnly,
@@ -2,6 +2,7 @@ interface RunnerOptions {
2
2
  configPath?: string;
3
3
  testDir?: string;
4
4
  pattern?: RegExp;
5
+ grep?: RegExp;
5
6
  timeoutMs?: number;
6
7
  bail?: boolean;
7
8
  debug?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"AAwBA,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA0ND,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,iBA8T5D;AA+DD,wBAAsB,aAAa,CAAC,OAAO,GAAE,aAAkB,iBAyD9D"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"AAyBA,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA+MD,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,iBAwU5D;AA+DD,wBAAsB,aAAa,CAAC,OAAO,GAAE,aAAkB,iBAyD9D"}
@@ -237,6 +237,8 @@ export async function runTestsCLI(options = {}) {
237
237
  files,
238
238
  timeoutMs,
239
239
  bail,
240
+ grep: options.grep,
241
+ retries: config?.retries,
240
242
  matchers: config?.matchers,
241
243
  });
242
244
  debugLog(options.debug, options.json, "Only mode:", engine.isOnly);
@@ -264,6 +266,7 @@ export async function runTestsCLI(options = {}) {
264
266
  description: result.description,
265
267
  status: result.status,
266
268
  durationMs: result.durationMs ?? 0,
269
+ attempts: result.attempts && result.attempts > 1 ? result.attempts : undefined,
267
270
  error: result.error
268
271
  ? { message: result.error.message, stack: result.error.stack }
269
272
  : undefined,
@@ -328,14 +331,17 @@ export async function runTestsCLI(options = {}) {
328
331
  lastSuitePath = suitePath;
329
332
  }
330
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
+ : "";
331
337
  if (result.status === TaskStatus.Pass) {
332
338
  const dur = colors.dim(formatDuration(result.durationMs ?? 0));
333
- console.log(`${indent}${colors.green("✔")} ${colors.dim(leaf)} ${dur}`);
339
+ console.log(`${indent}${colors.green("✔")} ${colors.dim(leaf)}${retried} ${dur}`);
334
340
  continue;
335
341
  }
336
342
  if (result.status === TaskStatus.Fail) {
337
343
  const dur = colors.dim(formatDuration(result.durationMs ?? 0));
338
- console.log(`${indent}${colors.red("✘")} ${colors.boldRed(leaf)} ${dur}`);
344
+ console.log(`${indent}${colors.red("✘")} ${colors.boldRed(leaf)}${retried} ${dur}`);
339
345
  continue;
340
346
  }
341
347
  if (result.status === TaskStatus.Skipped) {
@@ -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(): Array<{
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;
@@ -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;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"}
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
- try {
143
- for (const hook of beforeEachHooks) {
144
- try {
145
- await hook();
146
- }
147
- catch (error) {
148
- throw appendHookFailure(undefined, "beforeEach", toError(error));
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
- await test.run(options?.timeoutMs);
152
- }
153
- catch (error) {
154
- test.status = TaskStatus.Fail;
155
- test.error = toError(error);
156
- }
157
- finally {
158
- for (const hook of afterEachHooks) {
159
- try {
160
- await hook();
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
- catch (error) {
163
- test.status = TaskStatus.Fail;
164
- test.error = appendHookFailure(test.error, "afterEach", toError(error));
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) {
@@ -7,7 +7,11 @@ export declare class Test {
7
7
  mode: TaskMode;
8
8
  status: TaskStatus;
9
9
  timeoutMs?: number;
10
- constructor(description: string, fn: PromisableFn<void>, mode?: TaskMode, timeoutMs?: number);
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
@@ -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;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"}
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
@@ -13,11 +13,16 @@ export class Test {
13
13
  mode;
14
14
  status = TaskStatus.Pending;
15
15
  timeoutMs;
16
- constructor(description, fn, mode = TaskMode.Default, timeoutMs) {
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) {
17
21
  this.description = description;
18
22
  this.fn = fn;
19
23
  this.mode = mode;
20
24
  this.timeoutMs = timeoutMs;
25
+ this.retries = retries;
21
26
  }
22
27
  async run(globalTimeoutMs) {
23
28
  const timeoutMs = this.timeoutMs ?? globalTimeoutMs;
@@ -33,12 +38,14 @@ export class Test {
33
38
  }
34
39
  const start = realPerformanceNow();
35
40
  this.status = TaskStatus.Running;
41
+ this.timedOut = false;
36
42
  const exec = Promise.resolve().then(() => this.fn());
37
43
  try {
38
44
  if (timeoutMs !== undefined) {
39
45
  let timeoutId = null;
40
46
  const timeoutFn = new Promise((_, reject) => {
41
47
  timeoutId = setTimeout(() => {
48
+ this.timedOut = true;
42
49
  reject(new Error(`Test "${this.description}" timed out after ${timeoutMs} ms`));
43
50
  }, timeoutMs);
44
51
  });
@@ -16,6 +16,8 @@ export declare enum TaskMode {
16
16
  }
17
17
  export interface MatcherContext {
18
18
  isNot: boolean;
19
+ /** "resolves" or "rejects" under those modifiers, otherwise "" (same as Jest). */
20
+ promise: "" | "resolves" | "rejects";
19
21
  diff(a: unknown, b: unknown): string;
20
22
  }
21
23
  export type MatcherFn = (this: MatcherContext, received: unknown, ...args: readonly unknown[]) => void | Promise<void>;
@@ -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,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"}
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,kFAAkF;IAClF,OAAO,EAAE,EAAE,GAAG,UAAU,GAAG,SAAS,CAAC;IACrC,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"}
@@ -2,6 +2,7 @@ export interface UnriftConfigOptions {
2
2
  testDir?: string;
3
3
  timeoutMs?: number;
4
4
  bail?: boolean;
5
+ retries?: number;
5
6
  pattern?: string;
6
7
  includes?: string[];
7
8
  excludes?: string[];
@@ -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"}
@@ -30,5 +30,14 @@ export declare function regexTest(re: RegExp, value: string): boolean;
30
30
  * - +0 and -0 are equal
31
31
  */
32
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;
35
+ /** String() throws for e.g. null-prototype objects; fall back to the tag. */
36
+ export declare function safeString(value: unknown): string;
37
+ /**
38
+ * Error check that also works across realms (vm, workers, jsdom), where
39
+ * `instanceof Error` is false. Same test Jest uses.
40
+ */
41
+ export declare function isErrorLike(value: unknown): value is Error;
33
42
  export {};
34
43
  //# 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;;;;;;;;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"}
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;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAWjD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAW1D"}
@@ -124,3 +124,40 @@ export function sameValue(a, b) {
124
124
  Number.isNaN(a) &&
125
125
  Number.isNaN(b));
126
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
+ }
134
+ /** String() throws for e.g. null-prototype objects; fall back to the tag. */
135
+ export function safeString(value) {
136
+ try {
137
+ return String(value);
138
+ }
139
+ catch {
140
+ // The tag lookup can throw too (a throwing Symbol.toStringTag getter).
141
+ try {
142
+ return Object.prototype.toString.call(value);
143
+ }
144
+ catch {
145
+ return "[unprintable value]";
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * Error check that also works across realms (vm, workers, jsdom), where
151
+ * `instanceof Error` is false. Same test Jest uses.
152
+ */
153
+ export function isErrorLike(value) {
154
+ // Both checks can throw (Proxy traps, a throwing Symbol.toStringTag getter).
155
+ // A value that can't be inspected isn't treated as an Error.
156
+ try {
157
+ return (value instanceof Error ||
158
+ Object.prototype.toString.call(value) === "[object Error]");
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
@@ -7,5 +7,10 @@ interface MatchResult {
7
7
  message: () => string;
8
8
  }
9
9
  export declare function toThrow(received: unknown, expected?: ToThrowExpected, isNot?: boolean): MatchResult;
10
+ /**
11
+ * toThrow under `.resolves` / `.rejects`. `received` is the settled value, not
12
+ * a function. Same as Jest: it only counts as thrown if it's an Error.
13
+ */
14
+ export declare function toThrowFromPromise(received: unknown, expected: ToThrowExpected, isNot: boolean, promise: "resolves" | "rejects"): MatchResult;
10
15
  export {};
11
16
  //# sourceMappingURL=toThrow.d.ts.map
@@ -1 +1 @@
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
+ {"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;AA4ED,wBAAgB,OAAO,CACrB,QAAQ,EAAE,OAAO,EACjB,QAAQ,CAAC,EAAE,eAAe,EAC1B,KAAK,UAAQ,GACZ,WAAW,CAwDb;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,UAAU,GAAG,SAAS,GAC9B,WAAW,CAyBb"}
@@ -1,9 +1,9 @@
1
- import { regexTest } from "./helpers.js";
1
+ import { isErrorLike, regexTest, safeString } from "./helpers.js";
2
2
  function isErrorConstructor(err) {
3
3
  return typeof err === "function";
4
4
  }
5
5
  function formatThrown(thrown) {
6
- if (thrown instanceof Error) {
6
+ if (isErrorLike(thrown)) {
7
7
  const name = thrown.name || "Error";
8
8
  const msg = thrown.message ?? "";
9
9
  return msg ? `${name}: ${msg}` : name;
@@ -12,13 +12,22 @@ function formatThrown(thrown) {
12
12
  return typeof thrown === "string" ? thrown : JSON.stringify(thrown);
13
13
  }
14
14
  catch {
15
- return String(thrown);
15
+ return safeString(thrown);
16
16
  }
17
17
  }
18
+ function formatExpected(expected) {
19
+ return typeof expected === "string"
20
+ ? `"${expected}"`
21
+ : expected instanceof RegExp
22
+ ? expected.toString()
23
+ : isErrorConstructor(expected)
24
+ ? expected.name || "[AnonymousErrorConstructor]"
25
+ : JSON.stringify(expected, (_key, value) => value instanceof RegExp ? value.toString() : value);
26
+ }
18
27
  function matchThrown(thrown, expected) {
19
28
  if (expected === undefined)
20
29
  return true;
21
- const msg = thrown instanceof Error ? thrown.message : String(thrown);
30
+ const msg = isErrorLike(thrown) ? thrown.message : safeString(thrown);
22
31
  // toThrow("substring")
23
32
  if (typeof expected === "string") {
24
33
  return msg.includes(expected);
@@ -34,7 +43,7 @@ function matchThrown(thrown, expected) {
34
43
  // toThrow({ name?, message? })
35
44
  if (expected && typeof expected === "object") {
36
45
  if (expected.name) {
37
- const thrownName = thrown instanceof Error ? thrown.name : undefined;
46
+ const thrownName = isErrorLike(thrown) ? thrown.name : undefined;
38
47
  if (thrownName !== expected.name)
39
48
  return false;
40
49
  }
@@ -44,7 +53,7 @@ function matchThrown(thrown, expected) {
44
53
  return false;
45
54
  }
46
55
  else {
47
- if (!expected.message.test(msg))
56
+ if (!regexTest(expected.message, msg))
48
57
  return false;
49
58
  }
50
59
  }
@@ -73,7 +82,8 @@ export function toThrow(received, expected, isNot = false) {
73
82
  result != null &&
74
83
  typeof result.then === "function") {
75
84
  // Suppress the unhandled rejection from the returned promise
76
- result.catch(() => { });
85
+ // Promise.resolve so plain thenables without .catch work too
86
+ Promise.resolve(result).catch(() => { });
77
87
  return {
78
88
  pass: false,
79
89
  message: () => "toThrow() received an async function. Use expect(fn).rejects.toThrow() for async errors.",
@@ -92,14 +102,33 @@ export function toThrow(received, expected, isNot = false) {
92
102
  if (expected === undefined) {
93
103
  return `Expected function ${expectation}, but it threw: ${thrownStr}`;
94
104
  }
95
- const expectedStr = typeof expected === "string"
96
- ? `"${expected}"`
97
- : expected instanceof RegExp
98
- ? expected.toString()
99
- : isErrorConstructor(expected)
100
- ? expected.name || "[AnonymousErrorConstructor]"
101
- : JSON.stringify(expected);
102
- return `Expected function ${expectation} ${expectedStr}, but it threw: ${thrownStr}`;
105
+ return `Expected function ${expectation} ${formatExpected(expected)}, but it threw: ${thrownStr}`;
106
+ },
107
+ };
108
+ }
109
+ /**
110
+ * toThrow under `.resolves` / `.rejects`. `received` is the settled value, not
111
+ * a function. Same as Jest: it only counts as thrown if it's an Error.
112
+ */
113
+ export function toThrowFromPromise(received, expected, isNot, promise) {
114
+ // Same as Jest: a non-Error function is called and treated like a sync
115
+ // toThrow, so `resolves.toThrow()` works on a promise of a function.
116
+ if (typeof received === "function" && !isErrorLike(received)) {
117
+ return toThrow(received, expected, isNot);
118
+ }
119
+ const isError = isErrorLike(received);
120
+ const matched = isError && matchThrown(received, expected);
121
+ const pass = isNot ? !matched : matched;
122
+ return {
123
+ pass,
124
+ message: () => {
125
+ const verb = promise === "rejects" ? "reject" : "resolve";
126
+ const past = promise === "rejects" ? "rejected" : "resolved";
127
+ const target = expected === undefined ? "an Error" : formatExpected(expected);
128
+ const got = isError
129
+ ? formatThrown(received)
130
+ : `a non-Error value: ${formatThrown(received)}`;
131
+ return `Expected promise ${isNot ? "not " : ""}to ${verb} with ${target}, but it ${past} with ${got}`;
103
132
  },
104
133
  };
105
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unrift/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Core engine for the Unrift TypeScript test framework - runner, matchers, CLI, and programmatic API",
5
5
  "private": false,
6
6
  "sideEffects": false,