effect-bun-testing 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +462 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/internal.d.ts +82 -0
- package/dist/internal/internal.d.ts.map +1 -0
- package/dist/internal/internal.js +152 -0
- package/dist/internal/internal.js.map +1 -0
- package/dist/utils.d.ts +30 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +158 -0
- package/dist/utils.js.map +1 -0
- package/package.json +55 -0
- package/src/index.ts +106 -0
- package/src/internal/internal.ts +407 -0
- package/src/utils.ts +215 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Cause,
|
|
3
|
+
Duration,
|
|
4
|
+
Effect,
|
|
5
|
+
Exit,
|
|
6
|
+
Layer,
|
|
7
|
+
pipe,
|
|
8
|
+
Schedule,
|
|
9
|
+
Scope
|
|
10
|
+
} from "effect"
|
|
11
|
+
import { TestClock, TestConsole } from "effect/testing"
|
|
12
|
+
import {
|
|
13
|
+
describe,
|
|
14
|
+
beforeAll,
|
|
15
|
+
afterAll,
|
|
16
|
+
test as bunTest,
|
|
17
|
+
expect
|
|
18
|
+
} from "bun:test"
|
|
19
|
+
import * as fc from "fast-check"
|
|
20
|
+
import * as Equal from "effect/Equal"
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Types
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** A test body that returns an Effect. */
|
|
27
|
+
export interface TestFunction<A, E, R> {
|
|
28
|
+
(): Effect.Effect<A, E, R>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Registers a named test whose body returns an Effect. */
|
|
32
|
+
export interface Test<R> {
|
|
33
|
+
<A, E>(
|
|
34
|
+
name: string,
|
|
35
|
+
self: TestFunction<A, E, R>,
|
|
36
|
+
timeout?: number
|
|
37
|
+
): void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Full tester with modifiers (skip, only, each, etc.). */
|
|
41
|
+
export interface Tester<R> extends Test<R> {
|
|
42
|
+
readonly skip: Test<R>
|
|
43
|
+
readonly only: Test<R>
|
|
44
|
+
readonly skipIf: (condition: unknown) => Test<R>
|
|
45
|
+
readonly if: (condition: unknown) => Test<R>
|
|
46
|
+
/** Alias for `if` (vitest compat). */
|
|
47
|
+
readonly runIf: (condition: unknown) => Test<R>
|
|
48
|
+
readonly each: <T>(
|
|
49
|
+
cases: ReadonlyArray<T>
|
|
50
|
+
) => <A, E>(
|
|
51
|
+
name: string,
|
|
52
|
+
self: (args: T) => Effect.Effect<A, E, R>,
|
|
53
|
+
timeout?: number
|
|
54
|
+
) => void
|
|
55
|
+
readonly failing: Test<R>
|
|
56
|
+
/** Alias for `failing` (vitest compat). */
|
|
57
|
+
readonly fails: Test<R>
|
|
58
|
+
readonly prop: PropEffect<R>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Property-based test that returns an Effect. */
|
|
62
|
+
export interface PropEffect<R> {
|
|
63
|
+
<const Arbs extends ReadonlyArray<fc.Arbitrary<any>>>(
|
|
64
|
+
name: string,
|
|
65
|
+
arbitraries: Arbs,
|
|
66
|
+
self: (
|
|
67
|
+
...args: { [K in keyof Arbs]: Arbs[K] extends fc.Arbitrary<infer T> ? T : never }
|
|
68
|
+
) => Effect.Effect<any, any, R>,
|
|
69
|
+
timeout?: number | { readonly fastCheck?: fc.Parameters<any> }
|
|
70
|
+
): void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Property-based test that returns void / Promise<void>. */
|
|
74
|
+
export interface Prop {
|
|
75
|
+
<const Arbs extends ReadonlyArray<fc.Arbitrary<any>>>(
|
|
76
|
+
name: string,
|
|
77
|
+
arbitraries: Arbs,
|
|
78
|
+
self: (
|
|
79
|
+
...args: { [K in keyof Arbs]: Arbs[K] extends fc.Arbitrary<infer T> ? T : never }
|
|
80
|
+
) => void | Promise<void>,
|
|
81
|
+
timeout?: number | { readonly fastCheck?: fc.Parameters<any> }
|
|
82
|
+
): void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type TestServices = TestClock.TestClock | TestConsole.TestConsole
|
|
86
|
+
|
|
87
|
+
/** Full method set exposed on `it` and returned from `makeMethods`. */
|
|
88
|
+
export interface Methods {
|
|
89
|
+
readonly effect: Tester<TestServices | Scope.Scope>
|
|
90
|
+
readonly live: Tester<Scope.Scope>
|
|
91
|
+
readonly flakyTest: typeof flakyTest
|
|
92
|
+
readonly layer: typeof layer
|
|
93
|
+
readonly prop: Prop
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Method set available inside a `layer()` callback. */
|
|
97
|
+
export interface MethodsNonLive<R, ExcludeTestServices extends boolean = false> {
|
|
98
|
+
readonly effect: Tester<ExcludeTestServices extends true ? R | Scope.Scope : R | TestServices | Scope.Scope>
|
|
99
|
+
readonly flakyTest: typeof flakyTest
|
|
100
|
+
readonly layer: <R2, E2>(
|
|
101
|
+
layer_: Layer.Layer<R2, E2>,
|
|
102
|
+
options?: LayerOptions
|
|
103
|
+
) => LayerFn<R2, ExcludeTestServices>
|
|
104
|
+
readonly prop: Prop
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface LayerOptions {
|
|
108
|
+
readonly memoMap?: Layer.MemoMap
|
|
109
|
+
readonly timeout?: Duration.Input
|
|
110
|
+
readonly excludeTestServices?: boolean
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type LayerFn<R, ExcludeTestServices extends boolean = false> = {
|
|
114
|
+
(f: (it: MethodsNonLive<R, ExcludeTestServices>) => void): void
|
|
115
|
+
(name: string, f: (it: MethodsNonLive<R, ExcludeTestServices>) => void): void
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Test Environment
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const TestEnv: Layer.Layer<TestServices> = Layer.mergeAll(
|
|
123
|
+
TestConsole.layer,
|
|
124
|
+
TestClock.layer()
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// runPromise — Bun-adapted Effect runner (no signal, no onTestFinished)
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
const runPromise = <A, E>(effect: Effect.Effect<A, E, never>): Promise<A> =>
|
|
132
|
+
Effect.gen(function*() {
|
|
133
|
+
const exit = yield* Effect.exit(effect)
|
|
134
|
+
if (Exit.isSuccess(exit)) {
|
|
135
|
+
return exit.value
|
|
136
|
+
}
|
|
137
|
+
if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
138
|
+
throw new Error("All fibers interrupted without errors.")
|
|
139
|
+
}
|
|
140
|
+
const errors = Cause.prettyErrors(exit.cause)
|
|
141
|
+
for (let i = 1; i < errors.length; i++) {
|
|
142
|
+
yield* Effect.logError(errors[i])
|
|
143
|
+
}
|
|
144
|
+
throw errors[0]!
|
|
145
|
+
}).pipe(Effect.runPromise)
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// makeTester — creates a Tester<R> that maps each test body through mapEffect
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export const makeTester = <R>(
|
|
152
|
+
mapEffect: (self: Effect.Effect<any, any, R>) => Effect.Effect<any, any, never>,
|
|
153
|
+
testFn: typeof bunTest = bunTest
|
|
154
|
+
): Tester<R> => {
|
|
155
|
+
const run = <A, E>(self: () => Effect.Effect<A, E, R>): Promise<void> =>
|
|
156
|
+
runPromise(pipe(Effect.suspend(self), mapEffect, Effect.asVoid))
|
|
157
|
+
|
|
158
|
+
const f: Test<R> = (name, self, timeout) =>
|
|
159
|
+
testFn(name, () => run(self), timeout)
|
|
160
|
+
|
|
161
|
+
const skip: Test<R> = (name, self, timeout) =>
|
|
162
|
+
testFn.skip(name, () => run(self), timeout)
|
|
163
|
+
|
|
164
|
+
const only: Test<R> = (name, self, timeout) =>
|
|
165
|
+
testFn.only(name, () => run(self), timeout)
|
|
166
|
+
|
|
167
|
+
const skipIf =
|
|
168
|
+
(condition: unknown): Test<R> =>
|
|
169
|
+
(name, self, timeout) =>
|
|
170
|
+
testFn.skipIf(condition as boolean)(name, () => run(self), timeout)
|
|
171
|
+
|
|
172
|
+
const ifCond =
|
|
173
|
+
(condition: unknown): Test<R> =>
|
|
174
|
+
(name, self, timeout) =>
|
|
175
|
+
testFn.if(condition as boolean)(name, () => run(self), timeout)
|
|
176
|
+
|
|
177
|
+
const each =
|
|
178
|
+
<T>(cases: ReadonlyArray<T>) =>
|
|
179
|
+
<A, E>(
|
|
180
|
+
name: string,
|
|
181
|
+
self: (args: T) => Effect.Effect<A, E, R>,
|
|
182
|
+
timeout?: number
|
|
183
|
+
) =>
|
|
184
|
+
testFn.each(cases as T[])(
|
|
185
|
+
name,
|
|
186
|
+
(args: T) =>
|
|
187
|
+
runPromise(pipe(Effect.suspend(() => self(args)), mapEffect, Effect.asVoid)),
|
|
188
|
+
timeout
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const failing: Test<R> = (name, self, timeout) =>
|
|
192
|
+
testFn.failing(name, () => run(self), timeout)
|
|
193
|
+
|
|
194
|
+
const propFn: PropEffect<R> = (name, arbitraries, self, timeout) => {
|
|
195
|
+
const fcOptions =
|
|
196
|
+
typeof timeout === "object" ? timeout?.fastCheck : undefined
|
|
197
|
+
const timeoutMs = typeof timeout === "number" ? timeout : undefined
|
|
198
|
+
testFn(
|
|
199
|
+
name,
|
|
200
|
+
async () => {
|
|
201
|
+
await fc.assert(
|
|
202
|
+
fc.asyncProperty(
|
|
203
|
+
...(arbitraries as unknown as [fc.Arbitrary<any>]),
|
|
204
|
+
(...args: any[]) =>
|
|
205
|
+
runPromise(
|
|
206
|
+
pipe(
|
|
207
|
+
Effect.suspend(() => (self as any)(...args)) as Effect.Effect<any, any, R>,
|
|
208
|
+
mapEffect,
|
|
209
|
+
Effect.asVoid
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
),
|
|
213
|
+
fcOptions
|
|
214
|
+
)
|
|
215
|
+
},
|
|
216
|
+
timeoutMs
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return Object.assign(f, {
|
|
221
|
+
skip,
|
|
222
|
+
only,
|
|
223
|
+
skipIf,
|
|
224
|
+
if: ifCond,
|
|
225
|
+
runIf: ifCond,
|
|
226
|
+
each,
|
|
227
|
+
failing,
|
|
228
|
+
fails: failing,
|
|
229
|
+
prop: propFn
|
|
230
|
+
}) as Tester<R>
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Preconfigured testers
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/** Run effects with TestClock + TestConsole, auto-scoped. */
|
|
238
|
+
export const effect: Tester<TestServices | Scope.Scope> = makeTester((e) =>
|
|
239
|
+
e.pipe(Effect.scoped, Effect.provide(TestEnv))
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
/** Run effects live (no test services), auto-scoped. */
|
|
243
|
+
export const live: Tester<Scope.Scope> = makeTester((e) => Effect.scoped(e))
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// flakyTest — retry an effect up to 10 times within a timeout
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
export const flakyTest = <A, E, R>(
|
|
250
|
+
self: Effect.Effect<A, E, R>,
|
|
251
|
+
timeout: Duration.Input = Duration.seconds(30)
|
|
252
|
+
): Effect.Effect<A, never, R> =>
|
|
253
|
+
pipe(
|
|
254
|
+
Effect.catchDefect(self, (defect) => Effect.fail(defect as any)),
|
|
255
|
+
Effect.retry(
|
|
256
|
+
Schedule.both(Schedule.recurs(10), Schedule.during(timeout))
|
|
257
|
+
),
|
|
258
|
+
Effect.orDie
|
|
259
|
+
) as any
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// prop — non-Effect property-based test
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
export const prop: Prop = (name, arbitraries, self, timeout) => {
|
|
266
|
+
const fcOptions =
|
|
267
|
+
typeof timeout === "object" ? timeout?.fastCheck : undefined
|
|
268
|
+
const timeoutMs = typeof timeout === "number" ? timeout : undefined
|
|
269
|
+
bunTest(
|
|
270
|
+
name,
|
|
271
|
+
async () => {
|
|
272
|
+
await fc.assert(
|
|
273
|
+
fc.asyncProperty(
|
|
274
|
+
...(arbitraries as unknown as [fc.Arbitrary<any>]),
|
|
275
|
+
(...args: any[]) => (self as any)(...args)
|
|
276
|
+
),
|
|
277
|
+
fcOptions
|
|
278
|
+
)
|
|
279
|
+
},
|
|
280
|
+
timeoutMs
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// layer — shared Layer across tests with beforeAll/afterAll lifecycle
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
export const layer = <R, E, const ExcludeTestServices extends boolean = false>(
|
|
289
|
+
layer_: Layer.Layer<R, E>,
|
|
290
|
+
options?: {
|
|
291
|
+
readonly memoMap?: Layer.MemoMap
|
|
292
|
+
readonly timeout?: Duration.Input
|
|
293
|
+
readonly excludeTestServices?: ExcludeTestServices
|
|
294
|
+
}
|
|
295
|
+
): LayerFn<R, ExcludeTestServices> => {
|
|
296
|
+
type Env = ExcludeTestServices extends true ? R : R | TestServices
|
|
297
|
+
|
|
298
|
+
return ((...args: any[]) => {
|
|
299
|
+
const excludeTestServices = options?.excludeTestServices ?? false
|
|
300
|
+
const withTestEnv: Layer.Layer<any, any> = excludeTestServices
|
|
301
|
+
? (layer_ as any)
|
|
302
|
+
: Layer.provideMerge(layer_ as any, TestEnv as any)
|
|
303
|
+
const memoMap = options?.memoMap ?? Layer.makeMemoMapUnsafe()
|
|
304
|
+
const scope = Scope.makeUnsafe()
|
|
305
|
+
const timeout = options?.timeout
|
|
306
|
+
? Duration.toMillis(options.timeout)
|
|
307
|
+
: undefined
|
|
308
|
+
|
|
309
|
+
const serviceMapEffect = pipe(
|
|
310
|
+
Layer.buildWithMemoMap(withTestEnv, memoMap, scope),
|
|
311
|
+
Effect.orDie,
|
|
312
|
+
Effect.cached,
|
|
313
|
+
Effect.runSync
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const effectTester = makeTester<Env>(
|
|
317
|
+
(e) =>
|
|
318
|
+
Effect.flatMap(serviceMapEffect, (serviceMap) =>
|
|
319
|
+
e.pipe(Effect.scoped, Effect.provide(serviceMap))
|
|
320
|
+
) as any
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const makeIt = (): MethodsNonLive<R, ExcludeTestServices> =>
|
|
324
|
+
({
|
|
325
|
+
effect: effectTester,
|
|
326
|
+
flakyTest,
|
|
327
|
+
prop,
|
|
328
|
+
layer: <R2, E2>(
|
|
329
|
+
nestedLayer: Layer.Layer<R2, E2>,
|
|
330
|
+
nestedOptions?: LayerOptions
|
|
331
|
+
) =>
|
|
332
|
+
layer(
|
|
333
|
+
Layer.provideMerge(nestedLayer as any, withTestEnv) as any,
|
|
334
|
+
{
|
|
335
|
+
...nestedOptions,
|
|
336
|
+
memoMap,
|
|
337
|
+
excludeTestServices: true
|
|
338
|
+
} as any
|
|
339
|
+
)
|
|
340
|
+
}) as any
|
|
341
|
+
|
|
342
|
+
if (args.length === 1) {
|
|
343
|
+
beforeAll(
|
|
344
|
+
() => runPromise(Effect.asVoid(serviceMapEffect)),
|
|
345
|
+
timeout
|
|
346
|
+
)
|
|
347
|
+
afterAll(
|
|
348
|
+
() => runPromise(Scope.close(scope, Exit.void)),
|
|
349
|
+
timeout
|
|
350
|
+
)
|
|
351
|
+
return args[0](makeIt())
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return describe(args[0] as string, () => {
|
|
355
|
+
beforeAll(
|
|
356
|
+
() => runPromise(Effect.asVoid(serviceMapEffect)),
|
|
357
|
+
timeout
|
|
358
|
+
)
|
|
359
|
+
afterAll(
|
|
360
|
+
() => runPromise(Scope.close(scope, Exit.void)),
|
|
361
|
+
timeout
|
|
362
|
+
)
|
|
363
|
+
return args[1](makeIt())
|
|
364
|
+
})
|
|
365
|
+
}) as any
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// makeMethods — compose the full `Methods` object
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
export const makeMethods = (): Methods => ({
|
|
373
|
+
effect,
|
|
374
|
+
live,
|
|
375
|
+
flakyTest,
|
|
376
|
+
layer,
|
|
377
|
+
prop
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// addEqualityTesters — register Effect Equal-aware matchers via expect.extend
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
export const addEqualityTesters = (): void => {
|
|
385
|
+
expect.extend({
|
|
386
|
+
toEqualEffect(received: unknown, expected: unknown) {
|
|
387
|
+
if (!Equal.isEqual(received) || !Equal.isEqual(expected)) {
|
|
388
|
+
const pass = Object.is(received, expected)
|
|
389
|
+
return {
|
|
390
|
+
pass,
|
|
391
|
+
message: () =>
|
|
392
|
+
pass
|
|
393
|
+
? `Expected values to not be equal`
|
|
394
|
+
: `Expected values to be equal`
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const pass = Equal.equals(received, expected)
|
|
398
|
+
return {
|
|
399
|
+
pass,
|
|
400
|
+
message: () =>
|
|
401
|
+
pass
|
|
402
|
+
? `Expected Effect values to not be equal (via Equal.equals)`
|
|
403
|
+
: `Expected Effect values to be equal (via Equal.equals), but they differ`
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { expect } from "bun:test"
|
|
2
|
+
import * as Equal from "effect/Equal"
|
|
3
|
+
import { Option, Result, Exit, Cause } from "effect"
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Primitive assertions
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export function fail(message: string): never {
|
|
10
|
+
throw new Error(message)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function deepStrictEqual<A>(actual: A, expected: A, message?: string): void {
|
|
14
|
+
expect(actual).toStrictEqual(expected)
|
|
15
|
+
if (message) {
|
|
16
|
+
// Message is informational — the assertion above throws on failure
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function strictEqual<A>(actual: A, expected: A, message?: string): void {
|
|
21
|
+
expect(actual).toBe(expected)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function notDeepStrictEqual<A>(actual: A, expected: A, message?: string): void {
|
|
25
|
+
expect(actual).not.toStrictEqual(expected)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Compare using Effect's `Equal.equals` for structural equality. */
|
|
29
|
+
export function assertEquals<A>(actual: A, expected: A, message?: string): void {
|
|
30
|
+
if (!Equal.equals(actual, expected)) {
|
|
31
|
+
const msg = message ?? `Expected values to be equal (via Equal.equals)`
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${msg}\n actual: ${JSON.stringify(actual)}\n expected: ${JSON.stringify(expected)}`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function doesNotThrow(thunk: () => void, message?: string): void {
|
|
39
|
+
try {
|
|
40
|
+
thunk()
|
|
41
|
+
} catch (e) {
|
|
42
|
+
throw new Error(message ?? `Expected function not to throw, but it threw: ${e}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Boolean assertions
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export function assertTrue(self: unknown, message?: string): asserts self {
|
|
51
|
+
expect(self).toBeTruthy()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function assertFalse(self: boolean, message?: string): void {
|
|
55
|
+
expect(self).toBeFalsy()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Type assertions
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export function assertInstanceOf<C extends abstract new (...args: any[]) => any>(
|
|
63
|
+
value: unknown,
|
|
64
|
+
constructor: C,
|
|
65
|
+
message?: string
|
|
66
|
+
): asserts value is InstanceType<C> {
|
|
67
|
+
expect(value).toBeInstanceOf(constructor)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// String assertions
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
export function assertInclude(actual: string | undefined, expected: string): void {
|
|
75
|
+
expect(actual).toBeDefined()
|
|
76
|
+
expect(actual).toInclude(expected)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function assertMatch(actual: string, regexp: RegExp): void {
|
|
80
|
+
expect(actual).toMatch(regexp)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Throw assertions
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export function throws(
|
|
88
|
+
thunk: () => void,
|
|
89
|
+
error?: Error | ((u: unknown) => void)
|
|
90
|
+
): void {
|
|
91
|
+
if (typeof error === "function") {
|
|
92
|
+
try {
|
|
93
|
+
thunk()
|
|
94
|
+
throw new Error("Expected function to throw")
|
|
95
|
+
} catch (e) {
|
|
96
|
+
error(e)
|
|
97
|
+
}
|
|
98
|
+
} else if (error) {
|
|
99
|
+
expect(thunk).toThrow(error.message)
|
|
100
|
+
} else {
|
|
101
|
+
expect(thunk).toThrow()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function throwsAsync(
|
|
106
|
+
thunk: () => Promise<void>,
|
|
107
|
+
error?: Error | ((u: unknown) => void)
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
if (typeof error === "function") {
|
|
110
|
+
try {
|
|
111
|
+
await thunk()
|
|
112
|
+
throw new Error("Expected async function to throw")
|
|
113
|
+
} catch (e) {
|
|
114
|
+
error(e)
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
try {
|
|
118
|
+
await thunk()
|
|
119
|
+
throw new Error("Expected async function to throw")
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (error && e instanceof Error) {
|
|
122
|
+
expect(e.message).toInclude(error.message)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Option assertions
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export function assertNone<A>(
|
|
133
|
+
option: Option.Option<A>
|
|
134
|
+
): asserts option is Option.None<never> {
|
|
135
|
+
expect(Option.isNone(option)).toBe(true)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function assertSome<A>(
|
|
139
|
+
option: Option.Option<A>,
|
|
140
|
+
expected: A
|
|
141
|
+
): asserts option is Option.Some<A> {
|
|
142
|
+
expect(Option.isSome(option)).toBe(true)
|
|
143
|
+
if (Option.isSome(option)) {
|
|
144
|
+
assertEquals(option.value, expected)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Result assertions (v4: Either → Result)
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
export function assertSuccess_<A, E>(
|
|
153
|
+
result: Result.Result<A, E>,
|
|
154
|
+
expected: A
|
|
155
|
+
): asserts result is Result.Success<A, E> {
|
|
156
|
+
expect(Result.isSuccess(result)).toBe(true)
|
|
157
|
+
if (Result.isSuccess(result)) {
|
|
158
|
+
assertEquals(result.success, expected)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function assertFailure_<A, E>(
|
|
163
|
+
result: Result.Result<A, E>,
|
|
164
|
+
expected: E
|
|
165
|
+
): asserts result is Result.Failure<A, E> {
|
|
166
|
+
expect(Result.isFailure(result)).toBe(true)
|
|
167
|
+
if (Result.isFailure(result)) {
|
|
168
|
+
assertEquals(result.failure as any, expected as any)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Alias for assertSuccess_ (vitest compat: assertRight). */
|
|
173
|
+
export function assertRight<A, E>(
|
|
174
|
+
result: Result.Result<A, E>,
|
|
175
|
+
expected: A
|
|
176
|
+
): asserts result is Result.Success<A, E> {
|
|
177
|
+
assertSuccess_(result, expected)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Alias for assertFailure_ (vitest compat: assertLeft). */
|
|
181
|
+
export function assertLeft<A, E>(
|
|
182
|
+
result: Result.Result<A, E>,
|
|
183
|
+
expected: E
|
|
184
|
+
): asserts result is Result.Failure<A, E> {
|
|
185
|
+
assertFailure_(result, expected)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Exit assertions
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
export function assertExitSuccess<A, E>(
|
|
193
|
+
exit: Exit.Exit<A, E>,
|
|
194
|
+
expected: A
|
|
195
|
+
): asserts exit is Exit.Success<A, never> {
|
|
196
|
+
expect(Exit.isSuccess(exit)).toBe(true)
|
|
197
|
+
if (Exit.isSuccess(exit)) {
|
|
198
|
+
assertEquals(exit.value, expected)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function assertExitFailure<A, E>(
|
|
203
|
+
exit: Exit.Exit<A, E>,
|
|
204
|
+
expected: Cause.Cause<E>
|
|
205
|
+
): asserts exit is Exit.Failure<never, E> {
|
|
206
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
207
|
+
if (Exit.isFailure(exit)) {
|
|
208
|
+
assertEquals(exit.cause as any, expected as any)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @deprecated Use assertExitSuccess. Alias. */
|
|
213
|
+
export const assertSuccess = assertExitSuccess
|
|
214
|
+
/** @deprecated Use assertExitFailure. Alias. */
|
|
215
|
+
export const assertFailure = assertExitFailure
|