@c9up/helix 0.1.3
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/LICENSE +21 -0
- package/README.md +72 -0
- package/bin/helix.js +432 -0
- package/package.json +66 -0
- package/src/cli/coverage/aggregate.ts +231 -0
- package/src/cli/coverage/collect.ts +63 -0
- package/src/cli/coverage/diff/base.ts +46 -0
- package/src/cli/coverage/diff/index.ts +160 -0
- package/src/cli/coverage/diff/overlay.ts +62 -0
- package/src/cli/coverage/diff/parse.ts +121 -0
- package/src/cli/coverage/diff/reporters.ts +82 -0
- package/src/cli/coverage/diff/types.ts +46 -0
- package/src/cli/coverage/filter.ts +71 -0
- package/src/cli/coverage/glob.ts +0 -0
- package/src/cli/coverage/index.ts +126 -0
- package/src/cli/coverage/reporters/json.ts +40 -0
- package/src/cli/coverage/reporters/lcov.ts +54 -0
- package/src/cli/coverage/reporters/text.ts +48 -0
- package/src/cli/coverage/thresholds.ts +73 -0
- package/src/cli/coverage/types.ts +93 -0
- package/src/cli/discover.ts +174 -0
- package/src/cli/native.ts +104 -0
- package/src/cli/pool.ts +486 -0
- package/src/cli/reporter.ts +155 -0
- package/src/cli/run.ts +440 -0
- package/src/cli/summary.ts +42 -0
- package/src/cli/watch/loop.ts +159 -0
- package/src/cli/watch/types.ts +22 -0
- package/src/cli/watch/watcher.ts +145 -0
- package/src/container/index.ts +16 -0
- package/src/container/override.ts +86 -0
- package/src/container/spy.ts +25 -0
- package/src/index.ts +42 -0
- package/src/runtime/assertion-error.ts +38 -0
- package/src/runtime/cli-worker.ts +140 -0
- package/src/runtime/equals.ts +400 -0
- package/src/runtime/expect.ts +173 -0
- package/src/runtime/index.ts +50 -0
- package/src/runtime/lifecycle.ts +17 -0
- package/src/runtime/matchers.ts +452 -0
- package/src/runtime/run.ts +573 -0
- package/src/runtime/suite.ts +310 -0
- package/src/runtime/test-context.ts +59 -0
- package/src/runtime/vi/fake-timers.ts +410 -0
- package/src/runtime/vi/index.ts +254 -0
- package/src/runtime/vi/spy.ts +224 -0
- package/src/runtime/vi/spyOn.ts +155 -0
- package/src/runtime/vi/system-time.ts +121 -0
- package/src/runtime/worker.ts +239 -0
- package/src/time/freeze.ts +229 -0
- package/src/time/index.ts +16 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep structural equality — Vitest-compatible semantics.
|
|
3
|
+
*
|
|
4
|
+
* Handles: primitives (Object.is for NaN; ±0 treated equal in non-strict toEqual),
|
|
5
|
+
* Date, RegExp, Map, Set, Array, Buffer, typed arrays, DataView, boxed primitives
|
|
6
|
+
* (Number/String/Boolean), Error instances, circular refs, Symbol-keyed props.
|
|
7
|
+
* Ignores prototypes for plain objects (matches Jest/Vitest `toEqual`).
|
|
8
|
+
*
|
|
9
|
+
* `toStrictEqual` semantics (checks prototype + undefined-vs-missing keys)
|
|
10
|
+
* are supported via the `strict` option.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface EqualsOptions {
|
|
14
|
+
/** `toStrictEqual` semantics — prototypes matched, undefined keys distinguished from missing. */
|
|
15
|
+
strict?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Seen = WeakMap<object, WeakSet<object>>;
|
|
19
|
+
|
|
20
|
+
export function equals(
|
|
21
|
+
a: unknown,
|
|
22
|
+
b: unknown,
|
|
23
|
+
options: EqualsOptions = {},
|
|
24
|
+
): boolean {
|
|
25
|
+
return eq(a, b, options, new WeakMap());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Record a pair as "in progress" in BOTH directions so a later visit to the
|
|
30
|
+
* mirrored pair (b,a) is detected as a cycle too.
|
|
31
|
+
*/
|
|
32
|
+
function markSeen(seen: Seen, a: object, b: object): void {
|
|
33
|
+
let setA = seen.get(a);
|
|
34
|
+
if (!setA) {
|
|
35
|
+
setA = new WeakSet();
|
|
36
|
+
seen.set(a, setA);
|
|
37
|
+
}
|
|
38
|
+
setA.add(b);
|
|
39
|
+
let setB = seen.get(b);
|
|
40
|
+
if (!setB) {
|
|
41
|
+
setB = new WeakSet();
|
|
42
|
+
seen.set(b, setB);
|
|
43
|
+
}
|
|
44
|
+
setB.add(a);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function seenPair(seen: Seen, a: object, b: object): boolean {
|
|
48
|
+
return seen.get(a)?.has(b) ?? false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A structural comparator for one object "kind". Returns `true`/`false` when it
|
|
53
|
+
* recognises the pair as its kind (and decides equality), or `undefined` to
|
|
54
|
+
* mean "not my kind — let the next comparator try". This keeps `eq` a flat
|
|
55
|
+
* dispatch loop instead of a 150-line `instanceof` ladder.
|
|
56
|
+
*/
|
|
57
|
+
type Comparator = (
|
|
58
|
+
a: object,
|
|
59
|
+
b: object,
|
|
60
|
+
opts: EqualsOptions,
|
|
61
|
+
seen: Seen,
|
|
62
|
+
) => boolean | undefined;
|
|
63
|
+
|
|
64
|
+
function isBoxedPrimitive(v: object): boolean {
|
|
65
|
+
return v instanceof Number || v instanceof String || v instanceof Boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Boxed primitives — unwrap so `new Number(1)` vs `new Number(2)` differ
|
|
69
|
+
// properly; a box compared against anything not the same box type is unequal.
|
|
70
|
+
function compareBoxed(a: object, b: object): boolean | undefined {
|
|
71
|
+
if (a instanceof Number && b instanceof Number)
|
|
72
|
+
return Object.is(a.valueOf(), b.valueOf());
|
|
73
|
+
if (a instanceof String && b instanceof String)
|
|
74
|
+
return Object.is(a.valueOf(), b.valueOf());
|
|
75
|
+
if (a instanceof Boolean && b instanceof Boolean)
|
|
76
|
+
return Object.is(a.valueOf(), b.valueOf());
|
|
77
|
+
if (isBoxedPrimitive(a) || isBoxedPrimitive(b)) return false;
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function compareDate(a: object, b: object): boolean | undefined {
|
|
82
|
+
if (!(a instanceof Date) && !(b instanceof Date)) return undefined;
|
|
83
|
+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function compareRegExp(a: object, b: object): boolean | undefined {
|
|
87
|
+
if (!(a instanceof RegExp) && !(b instanceof RegExp)) return undefined;
|
|
88
|
+
return (
|
|
89
|
+
a instanceof RegExp &&
|
|
90
|
+
b instanceof RegExp &&
|
|
91
|
+
a.source === b.source &&
|
|
92
|
+
a.flags === b.flags
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Error instances — compare name + message + own enumerable keys. The built-in
|
|
97
|
+
// fields are non-enumerable so plain-keys comparison alone would report any two
|
|
98
|
+
// Errors as equal.
|
|
99
|
+
function compareError(
|
|
100
|
+
a: object,
|
|
101
|
+
b: object,
|
|
102
|
+
opts: EqualsOptions,
|
|
103
|
+
seen: Seen,
|
|
104
|
+
): boolean | undefined {
|
|
105
|
+
if (!(a instanceof Error) && !(b instanceof Error)) return undefined;
|
|
106
|
+
if (!(a instanceof Error && b instanceof Error)) return false;
|
|
107
|
+
if (a.name !== b.name) return false;
|
|
108
|
+
if (a.message !== b.message) return false;
|
|
109
|
+
return compareOwnKeys(a, b, opts, seen);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// DataView — compare by byteLength + getUint8 byte-for-byte.
|
|
113
|
+
function compareDataView(a: object, b: object): boolean | undefined {
|
|
114
|
+
if (!(a instanceof DataView) && !(b instanceof DataView)) return undefined;
|
|
115
|
+
if (!(a instanceof DataView && b instanceof DataView)) return false;
|
|
116
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
117
|
+
for (let i = 0; i < a.byteLength; i += 1) {
|
|
118
|
+
if (a.getUint8(i) !== b.getUint8(i)) return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Buffer — shortcut via Buffer.equals when both are Buffers. When only one is a
|
|
124
|
+
// Buffer, return `undefined` so the typed-array comparator handles it (a Buffer
|
|
125
|
+
// is a Uint8Array): a constructor mismatch there yields the correct inequality.
|
|
126
|
+
function compareBuffer(a: object, b: object): boolean | undefined {
|
|
127
|
+
if (typeof Buffer === "undefined") return undefined;
|
|
128
|
+
if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) return a.equals(b);
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Typed arrays (Uint8Array, Int16Array, Float64Array, BigInt64Array, …).
|
|
133
|
+
// Require matching constructor so Uint8Array !== Int8Array even when values coincide.
|
|
134
|
+
function compareTypedArray(
|
|
135
|
+
a: object,
|
|
136
|
+
b: object,
|
|
137
|
+
opts: EqualsOptions,
|
|
138
|
+
): boolean | undefined {
|
|
139
|
+
const viewA = asNumericTypedArray(a);
|
|
140
|
+
const viewB = asNumericTypedArray(b);
|
|
141
|
+
if (!viewA && !viewB) return undefined;
|
|
142
|
+
if (!viewA || !viewB) return false;
|
|
143
|
+
if (a.constructor !== b.constructor) return false;
|
|
144
|
+
if (viewA.length !== viewB.length) return false;
|
|
145
|
+
for (let i = 0; i < viewA.length; i += 1) {
|
|
146
|
+
const av = viewA[i];
|
|
147
|
+
const bv = viewB[i];
|
|
148
|
+
// Object.is so NaN === NaN inside Float32/64Array.
|
|
149
|
+
if (Object.is(av, bv)) continue;
|
|
150
|
+
if (opts.strict) return false;
|
|
151
|
+
if (av !== bv) return false; // non-strict: ±0 already caught by `===`.
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function compareArray(
|
|
157
|
+
a: object,
|
|
158
|
+
b: object,
|
|
159
|
+
opts: EqualsOptions,
|
|
160
|
+
seen: Seen,
|
|
161
|
+
): boolean | undefined {
|
|
162
|
+
if (!Array.isArray(a) && !Array.isArray(b)) return undefined;
|
|
163
|
+
if (!(Array.isArray(a) && Array.isArray(b))) return false;
|
|
164
|
+
if (a.length !== b.length) return false;
|
|
165
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
166
|
+
if (!eq(a[i], b[i], opts, seen)) return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Maps — structural key comparison supporting object keys.
|
|
172
|
+
function compareMap(
|
|
173
|
+
a: object,
|
|
174
|
+
b: object,
|
|
175
|
+
opts: EqualsOptions,
|
|
176
|
+
seen: Seen,
|
|
177
|
+
): boolean | undefined {
|
|
178
|
+
if (!(a instanceof Map) && !(b instanceof Map)) return undefined;
|
|
179
|
+
if (!(a instanceof Map && b instanceof Map)) return false;
|
|
180
|
+
if (a.size !== b.size) return false;
|
|
181
|
+
for (const [kA, vA] of a) {
|
|
182
|
+
// Fast path: identity/primitive key hit.
|
|
183
|
+
if (b.has(kA) && eq(vA, b.get(kA), opts, seen)) continue;
|
|
184
|
+
// Structural search across remaining entries.
|
|
185
|
+
let matched = false;
|
|
186
|
+
for (const [kB, vB] of b) {
|
|
187
|
+
if (eq(kA, kB, opts, seen) && eq(vA, vB, opts, seen)) {
|
|
188
|
+
matched = true;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!matched) return false;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Sets — deep structural. Use a fresh WeakMap per inner comparison so an
|
|
198
|
+
// earlier failed candidate does not poison the next one via cycle cache.
|
|
199
|
+
function compareSet(
|
|
200
|
+
a: object,
|
|
201
|
+
b: object,
|
|
202
|
+
opts: EqualsOptions,
|
|
203
|
+
): boolean | undefined {
|
|
204
|
+
if (!(a instanceof Set) && !(b instanceof Set)) return undefined;
|
|
205
|
+
if (!(a instanceof Set && b instanceof Set)) return false;
|
|
206
|
+
if (a.size !== b.size) return false;
|
|
207
|
+
for (const v of a) {
|
|
208
|
+
let found = false;
|
|
209
|
+
for (const w of b) {
|
|
210
|
+
if (eq(v, w, opts, new WeakMap())) {
|
|
211
|
+
found = true;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!found) return false;
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Order matters — mirrors the original `instanceof` ladder. Buffer sits before
|
|
221
|
+
// typed arrays so the both-Buffer fast path wins before the generic view check.
|
|
222
|
+
const STRUCTURAL_COMPARATORS: readonly Comparator[] = [
|
|
223
|
+
compareDate,
|
|
224
|
+
compareRegExp,
|
|
225
|
+
compareError,
|
|
226
|
+
compareDataView,
|
|
227
|
+
compareBuffer,
|
|
228
|
+
compareTypedArray,
|
|
229
|
+
compareArray,
|
|
230
|
+
compareMap,
|
|
231
|
+
compareSet,
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
function eq(a: unknown, b: unknown, opts: EqualsOptions, seen: Seen): boolean {
|
|
235
|
+
// Identity / NaN (Object.is). In non-strict mode, ±0 are also equal.
|
|
236
|
+
if (Object.is(a, b)) return true;
|
|
237
|
+
if (!opts.strict && a === b) return true;
|
|
238
|
+
if (a === null || b === null) return false;
|
|
239
|
+
if (typeof a !== typeof b) return false;
|
|
240
|
+
if (typeof a !== "object") return false;
|
|
241
|
+
|
|
242
|
+
const objA = a as object;
|
|
243
|
+
const objB = b as object;
|
|
244
|
+
|
|
245
|
+
if (seenPair(seen, objA, objB)) return true;
|
|
246
|
+
markSeen(seen, objA, objB);
|
|
247
|
+
|
|
248
|
+
const boxed = compareBoxed(objA, objB);
|
|
249
|
+
if (boxed !== undefined) return boxed;
|
|
250
|
+
|
|
251
|
+
// Strict: differing prototypes → not equal (plain `{}` vs class instance).
|
|
252
|
+
if (
|
|
253
|
+
opts.strict &&
|
|
254
|
+
Object.getPrototypeOf(objA) !== Object.getPrototypeOf(objB)
|
|
255
|
+
) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const compare of STRUCTURAL_COMPARATORS) {
|
|
260
|
+
const result = compare(objA, objB, opts, seen);
|
|
261
|
+
if (result !== undefined) return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return compareOwnKeys(objA, objB, opts, seen);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getProp(obj: object, key: PropertyKey): unknown {
|
|
268
|
+
return Reflect.get(obj, key);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
type NumericTypedArray =
|
|
272
|
+
| Int8Array
|
|
273
|
+
| Uint8Array
|
|
274
|
+
| Uint8ClampedArray
|
|
275
|
+
| Int16Array
|
|
276
|
+
| Uint16Array
|
|
277
|
+
| Int32Array
|
|
278
|
+
| Uint32Array
|
|
279
|
+
| Float32Array
|
|
280
|
+
| Float64Array
|
|
281
|
+
| BigInt64Array
|
|
282
|
+
| BigUint64Array;
|
|
283
|
+
|
|
284
|
+
function asNumericTypedArray(v: object): NumericTypedArray | undefined {
|
|
285
|
+
if (
|
|
286
|
+
v instanceof Int8Array ||
|
|
287
|
+
v instanceof Uint8Array ||
|
|
288
|
+
v instanceof Uint8ClampedArray ||
|
|
289
|
+
v instanceof Int16Array ||
|
|
290
|
+
v instanceof Uint16Array ||
|
|
291
|
+
v instanceof Int32Array ||
|
|
292
|
+
v instanceof Uint32Array ||
|
|
293
|
+
v instanceof Float32Array ||
|
|
294
|
+
v instanceof Float64Array ||
|
|
295
|
+
v instanceof BigInt64Array ||
|
|
296
|
+
v instanceof BigUint64Array
|
|
297
|
+
) {
|
|
298
|
+
return v;
|
|
299
|
+
}
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function compareOwnKeys(
|
|
304
|
+
objA: object,
|
|
305
|
+
objB: object,
|
|
306
|
+
opts: EqualsOptions,
|
|
307
|
+
seen: Seen,
|
|
308
|
+
): boolean {
|
|
309
|
+
const keysA = ownEnumerableKeys(objA);
|
|
310
|
+
const keysB = ownEnumerableKeys(objB);
|
|
311
|
+
|
|
312
|
+
if (opts.strict) {
|
|
313
|
+
if (keysA.length !== keysB.length) return false;
|
|
314
|
+
for (const k of keysA) {
|
|
315
|
+
if (!Object.prototype.propertyIsEnumerable.call(objB, k)) return false;
|
|
316
|
+
if (!eq(getProp(objA, k), getProp(objB, k), opts, seen)) return false;
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Non-strict: undefined value ≡ missing key.
|
|
322
|
+
const union = new Set<string | symbol>([...keysA, ...keysB]);
|
|
323
|
+
for (const k of union) {
|
|
324
|
+
const va = getProp(objA, k);
|
|
325
|
+
const vb = getProp(objB, k);
|
|
326
|
+
if (va === undefined && vb === undefined) continue;
|
|
327
|
+
if (!eq(va, vb, opts, seen)) return false;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function ownEnumerableKeys(obj: object): Array<string | symbol> {
|
|
333
|
+
const strings = Object.keys(obj);
|
|
334
|
+
const symbols = Object.getOwnPropertySymbols(obj).filter((s) =>
|
|
335
|
+
Object.prototype.propertyIsEnumerable.call(obj, s),
|
|
336
|
+
);
|
|
337
|
+
return [...strings, ...symbols];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Partial match — every key/element in `expected` must match in `actual`.
|
|
342
|
+
* Extra keys in `actual` are allowed for plain objects.
|
|
343
|
+
* Arrays must have the same length (Vitest v1+ semantics, not Jest).
|
|
344
|
+
* Date / RegExp / Error / Map / Set / TypedArray / Buffer / DataView delegate
|
|
345
|
+
* to `equals` — "partial" is ill-defined for them.
|
|
346
|
+
*/
|
|
347
|
+
export function partialEquals(actual: unknown, expected: unknown): boolean {
|
|
348
|
+
return partial(actual, expected, new WeakMap());
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function partial(actual: unknown, expected: unknown, seen: Seen): boolean {
|
|
352
|
+
if (Object.is(actual, expected)) return true;
|
|
353
|
+
if (expected === null || expected === undefined) {
|
|
354
|
+
return actual === expected;
|
|
355
|
+
}
|
|
356
|
+
if (typeof expected !== "object") {
|
|
357
|
+
return equals(actual, expected);
|
|
358
|
+
}
|
|
359
|
+
if (typeof actual !== "object" || actual === null) return false;
|
|
360
|
+
|
|
361
|
+
// Both narrowed to object by the guards above — no cast needed.
|
|
362
|
+
const objE: object = expected;
|
|
363
|
+
const objA: object = actual;
|
|
364
|
+
|
|
365
|
+
if (seenPair(seen, objA, objE)) return true;
|
|
366
|
+
markSeen(seen, objA, objE);
|
|
367
|
+
|
|
368
|
+
// Types with no "partial" semantics — fall through to full equality.
|
|
369
|
+
if (lacksPartialSemantics(objE)) {
|
|
370
|
+
return equals(actual, expected);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (Array.isArray(expected)) {
|
|
374
|
+
if (!Array.isArray(actual)) return false;
|
|
375
|
+
if (actual.length !== expected.length) return false;
|
|
376
|
+
for (let i = 0; i < expected.length; i += 1) {
|
|
377
|
+
if (!partial(actual[i], expected[i], seen)) return false;
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for (const key of ownEnumerableKeys(objE)) {
|
|
383
|
+
if (!partial(getProp(objA, key), getProp(objE, key), seen)) return false;
|
|
384
|
+
}
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Object kinds for which "partial" is ill-defined → fall back to full equals. */
|
|
389
|
+
function lacksPartialSemantics(value: object): boolean {
|
|
390
|
+
return (
|
|
391
|
+
value instanceof Date ||
|
|
392
|
+
value instanceof RegExp ||
|
|
393
|
+
value instanceof Error ||
|
|
394
|
+
value instanceof Map ||
|
|
395
|
+
value instanceof Set ||
|
|
396
|
+
value instanceof DataView ||
|
|
397
|
+
(typeof Buffer !== "undefined" && Buffer.isBuffer(value)) ||
|
|
398
|
+
(ArrayBuffer.isView(value) && !(value instanceof DataView))
|
|
399
|
+
);
|
|
400
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `expect(value)` engine — chainable assertion API compatible with Vitest.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - All matchers in `./matchers.ts`
|
|
6
|
+
* - `.not.<matcher>()` — inverts pass/fail
|
|
7
|
+
* - `.resolves.<matcher>()` / `.rejects.<matcher>()` — async awaits on received
|
|
8
|
+
* - All four combinations: `.resolves.not`, `.rejects.not`, `.not.resolves`, `.not.rejects`
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AssertionError } from "./assertion-error.js";
|
|
12
|
+
import { type MatcherName, matchers } from "./matchers.js";
|
|
13
|
+
|
|
14
|
+
type MatcherArgs<Name extends MatcherName> = (typeof matchers)[Name] extends (
|
|
15
|
+
received: unknown,
|
|
16
|
+
...args: infer A
|
|
17
|
+
) => unknown
|
|
18
|
+
? A
|
|
19
|
+
: never;
|
|
20
|
+
|
|
21
|
+
type SyncChain = {
|
|
22
|
+
[K in MatcherName]: (...args: MatcherArgs<K>) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type AsyncChain = {
|
|
26
|
+
[K in MatcherName]: (...args: MatcherArgs<K>) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type Assertion = SyncChain & {
|
|
30
|
+
not: SyncChain & {
|
|
31
|
+
resolves: AsyncChain;
|
|
32
|
+
rejects: AsyncChain;
|
|
33
|
+
};
|
|
34
|
+
resolves: AsyncChain & { not: AsyncChain };
|
|
35
|
+
rejects: AsyncChain & { not: AsyncChain };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface EvalContext {
|
|
39
|
+
received: unknown;
|
|
40
|
+
negate: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type MatcherInvoker = (
|
|
44
|
+
received: unknown,
|
|
45
|
+
...rest: unknown[]
|
|
46
|
+
) => {
|
|
47
|
+
pass: boolean;
|
|
48
|
+
message(): string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function evaluate(ctx: EvalContext, name: MatcherName, args: unknown[]): void {
|
|
52
|
+
const matcher = matchers[name] as MatcherInvoker;
|
|
53
|
+
let result: { pass: boolean; message(): string };
|
|
54
|
+
try {
|
|
55
|
+
result = matcher(ctx.received, ...args);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// A matcher threw unexpectedly (e.g. a user-supplied constructor with
|
|
58
|
+
// a throwing `Symbol.hasInstance`). Surface it as a normal assertion
|
|
59
|
+
// failure so reporters can render it uniformly.
|
|
60
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
61
|
+
throw new AssertionError({
|
|
62
|
+
message: `matcher ${name} threw: ${why}`,
|
|
63
|
+
actual: ctx.received,
|
|
64
|
+
expected: args.length === 1 ? args[0] : args,
|
|
65
|
+
operator: ctx.negate ? `not.${name}` : name,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const pass = ctx.negate ? !result.pass : result.pass;
|
|
69
|
+
if (pass) return;
|
|
70
|
+
const prefix = ctx.negate ? "expected NOT: " : "";
|
|
71
|
+
throw new AssertionError({
|
|
72
|
+
message: `${prefix}${result.message()}`,
|
|
73
|
+
actual: ctx.received,
|
|
74
|
+
expected: args,
|
|
75
|
+
operator: ctx.negate ? `not.${name}` : name,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const matcherNames = Object.keys(matchers) as MatcherName[];
|
|
80
|
+
|
|
81
|
+
function buildSync(received: unknown, negate: boolean): SyncChain {
|
|
82
|
+
const api = {} as SyncChain;
|
|
83
|
+
for (const name of matcherNames) {
|
|
84
|
+
const fn = (...args: unknown[]) =>
|
|
85
|
+
evaluate({ received, negate }, name, args);
|
|
86
|
+
Reflect.set(api, name, fn);
|
|
87
|
+
}
|
|
88
|
+
return api;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type PromiseMode = "resolves" | "rejects";
|
|
92
|
+
|
|
93
|
+
function buildAsync(
|
|
94
|
+
received: unknown,
|
|
95
|
+
mode: PromiseMode,
|
|
96
|
+
negate: boolean,
|
|
97
|
+
): AsyncChain {
|
|
98
|
+
const api = {} as AsyncChain;
|
|
99
|
+
for (const name of matcherNames) {
|
|
100
|
+
const fn = async (...args: unknown[]) => {
|
|
101
|
+
if (
|
|
102
|
+
!received ||
|
|
103
|
+
typeof (received as { then?: unknown }).then !== "function"
|
|
104
|
+
) {
|
|
105
|
+
throw new AssertionError({
|
|
106
|
+
message: `expected a Promise (for .${mode}), got ${typeof received}`,
|
|
107
|
+
operator: `${mode}.${name}`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
let resolved: unknown;
|
|
111
|
+
let rejected: unknown;
|
|
112
|
+
let didReject = false;
|
|
113
|
+
try {
|
|
114
|
+
resolved = await (received as PromiseLike<unknown>);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
rejected = err;
|
|
117
|
+
didReject = true;
|
|
118
|
+
}
|
|
119
|
+
if (mode === "resolves") {
|
|
120
|
+
if (didReject) {
|
|
121
|
+
throw new AssertionError({
|
|
122
|
+
message: `expected promise to resolve, but it rejected with ${String(rejected)}`,
|
|
123
|
+
operator: `resolves.${name}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
evaluate({ received: resolved, negate }, name, args);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!didReject) {
|
|
130
|
+
throw new AssertionError({
|
|
131
|
+
message: `expected promise to reject, but it resolved with ${String(resolved)}`,
|
|
132
|
+
operator: `rejects.${name}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// `.rejects.toThrow(...)` feeds the rejection into toThrow's
|
|
136
|
+
// function-scoped contract by wrapping it in a rethrowing thunk.
|
|
137
|
+
const target =
|
|
138
|
+
name === "toThrow"
|
|
139
|
+
? () => {
|
|
140
|
+
throw rejected;
|
|
141
|
+
}
|
|
142
|
+
: rejected;
|
|
143
|
+
evaluate({ received: target, negate }, name, args);
|
|
144
|
+
};
|
|
145
|
+
Reflect.set(api, name, fn);
|
|
146
|
+
}
|
|
147
|
+
return api;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function expect(received: unknown): Assertion {
|
|
151
|
+
const base = buildSync(received, false);
|
|
152
|
+
|
|
153
|
+
// `.not` returns a chain that negates every matcher. It also exposes
|
|
154
|
+
// `.resolves` / `.rejects` so `expect(p).not.resolves.toBe(x)` works, matching
|
|
155
|
+
// Vitest's full surface.
|
|
156
|
+
const notSync = Object.assign(buildSync(received, true), {
|
|
157
|
+
resolves: buildAsync(received, "resolves", true),
|
|
158
|
+
rejects: buildAsync(received, "rejects", true),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const resolves = Object.assign(buildAsync(received, "resolves", false), {
|
|
162
|
+
not: buildAsync(received, "resolves", true),
|
|
163
|
+
});
|
|
164
|
+
const rejects = Object.assign(buildAsync(received, "rejects", false), {
|
|
165
|
+
not: buildAsync(received, "rejects", true),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return Object.assign(base, {
|
|
169
|
+
not: notSync,
|
|
170
|
+
resolves,
|
|
171
|
+
rejects,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@c9up/helix/runtime` — public surface of the Vitest-compatible test runtime.
|
|
3
|
+
*
|
|
4
|
+
* Test files can `import { describe, test, expect } from "@c9up/helix"` and
|
|
5
|
+
* get the full DSL. The runtime is orchestrator-agnostic: a Node child
|
|
6
|
+
* process, a Bun worker, or a Rust-spawned worker all load the same module.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type { AssertionErrorInit } from "./assertion-error.js";
|
|
10
|
+
export { AssertionError, isAssertionError } from "./assertion-error.js";
|
|
11
|
+
export type { EqualsOptions } from "./equals.js";
|
|
12
|
+
export { equals, partialEquals } from "./equals.js";
|
|
13
|
+
export type { Assertion } from "./expect.js";
|
|
14
|
+
export { expect } from "./expect.js";
|
|
15
|
+
export type { Hook, HookType } from "./lifecycle.js";
|
|
16
|
+
export {
|
|
17
|
+
addHook,
|
|
18
|
+
afterAll,
|
|
19
|
+
afterEach,
|
|
20
|
+
beforeAll,
|
|
21
|
+
beforeEach,
|
|
22
|
+
} from "./lifecycle.js";
|
|
23
|
+
export type { MatcherName, MatcherResult, SpyLike } from "./matchers.js";
|
|
24
|
+
export { matchers } from "./matchers.js";
|
|
25
|
+
export type {
|
|
26
|
+
FileResult,
|
|
27
|
+
SerializedError,
|
|
28
|
+
SuiteResult,
|
|
29
|
+
TestResult,
|
|
30
|
+
} from "./run.js";
|
|
31
|
+
export { executeRoot } from "./run.js";
|
|
32
|
+
export type { RunMode, SuiteNode, TestFn, TestNode } from "./suite.js";
|
|
33
|
+
export {
|
|
34
|
+
describe,
|
|
35
|
+
getRoot,
|
|
36
|
+
it,
|
|
37
|
+
resetRoot,
|
|
38
|
+
test,
|
|
39
|
+
} from "./suite.js";
|
|
40
|
+
export type { Vi } from "./vi/index.js";
|
|
41
|
+
export { vi, withViContext } from "./vi/index.js";
|
|
42
|
+
export type {
|
|
43
|
+
CreateSpyOptions,
|
|
44
|
+
MockCallResult,
|
|
45
|
+
MockInternals,
|
|
46
|
+
Spy,
|
|
47
|
+
} from "./vi/spy.js";
|
|
48
|
+
export type { SpyOnOptions } from "./vi/spyOn.js";
|
|
49
|
+
export type { RunFileOptions } from "./worker.js";
|
|
50
|
+
export { runTestFile } from "./worker.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public re-exports for lifecycle hooks.
|
|
3
|
+
*
|
|
4
|
+
* The actual registration lives in `./suite.ts` (hooks are attached to the
|
|
5
|
+
* currently-active suite on the collection stack). This module exists as a
|
|
6
|
+
* stable import path for reporters/plugins that want hooks without the full
|
|
7
|
+
* DSL surface.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type { Hook, HookType } from "./suite.js";
|
|
11
|
+
export {
|
|
12
|
+
addHook,
|
|
13
|
+
afterAll,
|
|
14
|
+
afterEach,
|
|
15
|
+
beforeAll,
|
|
16
|
+
beforeEach,
|
|
17
|
+
} from "./suite.js";
|