@plugjs/expect5 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/cli.mjs +2 -2
  2. package/dist/cli.mjs.map +1 -1
  3. package/dist/execution/executor.cjs.map +1 -1
  4. package/dist/execution/executor.d.ts +1 -1
  5. package/dist/execution/executor.mjs +1 -1
  6. package/dist/execution/executor.mjs.map +1 -1
  7. package/dist/execution/setup.cjs.map +1 -1
  8. package/dist/execution/setup.d.ts +1 -1
  9. package/dist/execution/setup.mjs +1 -1
  10. package/dist/execution/setup.mjs.map +1 -1
  11. package/dist/expectation/async.cjs +78 -45
  12. package/dist/expectation/async.cjs.map +1 -1
  13. package/dist/expectation/async.d.ts +66 -51
  14. package/dist/expectation/async.mjs +79 -42
  15. package/dist/expectation/async.mjs.map +1 -1
  16. package/dist/expectation/diff.cjs.map +1 -1
  17. package/dist/expectation/diff.mjs +6 -1
  18. package/dist/expectation/diff.mjs.map +1 -1
  19. package/dist/expectation/expect.cjs +11 -165
  20. package/dist/expectation/expect.cjs.map +2 -2
  21. package/dist/expectation/expect.d.ts +10 -112
  22. package/dist/expectation/expect.mjs +12 -207
  23. package/dist/expectation/expect.mjs.map +2 -2
  24. package/dist/expectation/expectations.cjs +549 -0
  25. package/dist/expectation/expectations.cjs.map +6 -0
  26. package/dist/expectation/expectations.d.ts +454 -0
  27. package/dist/expectation/expectations.mjs +530 -0
  28. package/dist/expectation/expectations.mjs.map +6 -0
  29. package/dist/expectation/include.cjs +43 -41
  30. package/dist/expectation/include.cjs.map +1 -1
  31. package/dist/expectation/include.d.ts +3 -19
  32. package/dist/expectation/include.mjs +43 -41
  33. package/dist/expectation/include.mjs.map +1 -1
  34. package/dist/expectation/matchers.cjs +350 -0
  35. package/dist/expectation/matchers.cjs.map +6 -0
  36. package/dist/expectation/matchers.d.ts +375 -0
  37. package/dist/expectation/matchers.mjs +328 -0
  38. package/dist/expectation/matchers.mjs.map +6 -0
  39. package/dist/expectation/print.cjs.map +1 -1
  40. package/dist/expectation/print.d.ts +2 -2
  41. package/dist/expectation/print.mjs.map +1 -1
  42. package/dist/expectation/types.cjs +17 -23
  43. package/dist/expectation/types.cjs.map +1 -1
  44. package/dist/expectation/types.d.ts +7 -51
  45. package/dist/expectation/types.mjs +17 -22
  46. package/dist/expectation/types.mjs.map +1 -1
  47. package/dist/globals.d.ts +2 -2
  48. package/dist/index.cjs +5 -5
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.ts +3 -4
  51. package/dist/index.mjs +11 -12
  52. package/dist/index.mjs.map +1 -1
  53. package/dist/test.cjs +33 -5
  54. package/dist/test.cjs.map +1 -1
  55. package/dist/test.d.ts +2 -2
  56. package/dist/test.mjs +34 -6
  57. package/dist/test.mjs.map +1 -1
  58. package/package.json +2 -2
  59. package/src/cli.mts +1 -1
  60. package/src/execution/executor.ts +1 -3
  61. package/src/execution/setup.ts +1 -3
  62. package/src/expectation/async.ts +145 -145
  63. package/src/expectation/diff.ts +7 -3
  64. package/src/expectation/expect.ts +22 -362
  65. package/src/expectation/expectations.ts +971 -0
  66. package/src/expectation/include.ts +59 -75
  67. package/src/expectation/matchers.ts +698 -0
  68. package/src/expectation/print.ts +8 -4
  69. package/src/expectation/types.ts +22 -94
  70. package/src/globals.ts +2 -2
  71. package/src/index.ts +17 -10
  72. package/src/test.ts +33 -10
  73. package/dist/expectation/basic.cjs +0 -188
  74. package/dist/expectation/basic.cjs.map +0 -6
  75. package/dist/expectation/basic.d.ts +0 -90
  76. package/dist/expectation/basic.mjs +0 -156
  77. package/dist/expectation/basic.mjs.map +0 -6
  78. package/dist/expectation/throwing.cjs +0 -58
  79. package/dist/expectation/throwing.cjs.map +0 -6
  80. package/dist/expectation/throwing.d.ts +0 -36
  81. package/dist/expectation/throwing.mjs +0 -32
  82. package/dist/expectation/throwing.mjs.map +0 -6
  83. package/dist/expectation/trivial.cjs +0 -96
  84. package/dist/expectation/trivial.cjs.map +0 -6
  85. package/dist/expectation/trivial.d.ts +0 -13
  86. package/dist/expectation/trivial.mjs +0 -61
  87. package/dist/expectation/trivial.mjs.map +0 -6
  88. package/src/expectation/basic.ts +0 -413
  89. package/src/expectation/throwing.ts +0 -106
  90. package/src/expectation/trivial.ts +0 -107
@@ -0,0 +1,971 @@
1
+ import { diff, type Diff } from './diff'
2
+ import { toInclude, toMatchContents } from './include'
3
+ import { type Matchers } from './matchers'
4
+ import {
5
+ ExpectationError,
6
+ isMatcher,
7
+ prefixType,
8
+ stringifyConstructor,
9
+ stringifyValue,
10
+ typeOf,
11
+ type Constructor,
12
+ type TypeMappings,
13
+ type TypeName,
14
+ } from './types'
15
+
16
+ /* ========================================================================== *
17
+ * TYPES SUPPORTING EXPECTATIONS *
18
+ * ========================================================================== */
19
+
20
+ /** An assertion function, for sub-expectations and value introspection */
21
+ export type AssertionFunction<T = unknown> = (assert: Expectations<T>) => void | Expectations
22
+
23
+ /** Return the type asserted by an {@link AssertionFunction} or `T` */
24
+ export type AssertedType<T, F extends AssertionFunction<any>, R = ReturnType<F>> =
25
+ R extends Expectations<infer I> ?
26
+ unknown extends I ?
27
+ T : // returns Expectations<unknown>, use T
28
+ I : // returns Expectations<something>, use "something"
29
+ T // returns something else (void), use T
30
+
31
+ export type InferMatchers<T> =
32
+ T extends Matchers<infer V> ? V :
33
+ T extends Record<any, any> ? { [ k in keyof T ] : InferMatchers<T[k]> } :
34
+ T
35
+
36
+ /** Simple wrapper defining the _parent_ instance of an {@link Expectations}. */
37
+ type ExpectationsParent = {
38
+ /** Parent {@link Expectations} instance */
39
+ expectations: Expectations,
40
+ /** Property associating _this_ to the parent */
41
+ prop: string | number | symbol,
42
+ }
43
+
44
+ /* ========================================================================== *
45
+ * EXPECTATIONS *
46
+ * ========================================================================== */
47
+
48
+ /** Main class containing all supported expectations */
49
+ export class Expectations<T = unknown> {
50
+ /** The {@link NegativeExpectations} associated with _this_ */
51
+ readonly not: NegativeExpectations<T>
52
+
53
+ /**
54
+ * Create an {@link Expectations} instance associated with the specified
55
+ * value and error remarks.
56
+ *
57
+ * Optionally a parent {@link Expectations} instance can be specified.
58
+ */
59
+ constructor(
60
+ /** The value associated with this instance */
61
+ readonly value: T,
62
+ /** Optional additional _remarks_ to associate with errors */
63
+ readonly remarks: string | undefined, // required, but migth be undefined
64
+ /**
65
+ * An optional {@link ExpectationsParent} defining _this_ to be a
66
+ * child of another {@link Expectations} instance
67
+ */
68
+ readonly parent?: ExpectationsParent,
69
+ ) {
70
+ this.not = new NegativeExpectations(this)
71
+ }
72
+
73
+ /** Throw an {@link ExpectationError} associated with _this_ */
74
+ protected _fail(details: string, diff?: Diff): never {
75
+ throw new ExpectationError(this, details, diff)
76
+ }
77
+
78
+ /* ------------------------------------------------------------------------ *
79
+ * BASIC *
80
+ * ------------------------------------------------------------------------ */
81
+
82
+ /**
83
+ * Expects the value to be of the specified _extended_ {@link TypeName type},
84
+ * and (if specified) further asserts it with an {@link AssertionFunction}.
85
+ *
86
+ * Negation: {@link NegativeExpectations.toBeA `not.toBeA(...)`}
87
+ */
88
+ toBeA<
89
+ Name extends TypeName,
90
+ Mapped extends TypeMappings[Name],
91
+ Assert extends AssertionFunction<Mapped>,
92
+ >(
93
+ type: Name,
94
+ assertion?: Assert,
95
+ ): Expectations<AssertedType<Mapped, Assert>> {
96
+ if (typeOf(this.value) === type) {
97
+ if (assertion) assertion(this as Expectations<any>)
98
+ return this as Expectations<any>
99
+ }
100
+
101
+ this._fail(`to be ${prefixType(type)}`)
102
+ }
103
+
104
+ /* ------------------------------------------------------------------------ */
105
+
106
+ /**
107
+ * Expects the value to be a `number` within a given +/- _delta_ range of the
108
+ * specified expected value.
109
+ *
110
+ * Negation: {@link NegativeExpectations.toBeCloseTo `not.toBeCloseTo(...)`}
111
+ */
112
+ toBeCloseTo(value: number, delta: number): Expectations<number>
113
+
114
+ /**
115
+ * Expects the value to be a `bigint` within a given +/- _delta_ range of the
116
+ * specified expected value.
117
+ *
118
+ * Negation: {@link NegativeExpectations.toBeCloseTo `not.toBeCloseTo(...)`}
119
+ */
120
+ toBeCloseTo(value: bigint, delta: bigint): Expectations<bigint>
121
+
122
+ toBeCloseTo(value: number | bigint, delta: number | bigint): Expectations {
123
+ const min = (value as number) - (delta as number)
124
+ const max = (value as number) + (delta as number)
125
+ return this.toBeWithinRange(min, max)
126
+ }
127
+
128
+ /* ------------------------------------------------------------------------ */
129
+
130
+ /**
131
+ * Expects the value to be neither `null` nor `undefined`.
132
+ *
133
+ * Negation: {@link NegativeExpectations.toBeDefined `not.toBeDefined()`}
134
+ */
135
+ toBeDefined(): Expectations<T> {
136
+ if ((this.value !== null) && (this.value !== undefined)) return this
137
+ this._fail(`to be neither ${stringifyValue(null)} nor ${stringifyValue(undefined)}`)
138
+ }
139
+
140
+ /* ------------------------------------------------------------------------ */
141
+
142
+ /**
143
+ * Expect the value to be an instance of {@link Error}.
144
+ *
145
+ * If specified, the {@link Error}'s own message will be further expected to
146
+ * either match the specified {@link RegExp}, or equal the specified `string`.
147
+ */
148
+ toBeError(
149
+ message?: string | RegExp
150
+ ): Expectations<Error>
151
+
152
+ /**
153
+ * Expect the value to be an instance of {@link Error} and further asserts
154
+ * it to be an instance of the specifed {@link Error} {@link Constructor}.
155
+ *
156
+ * If specified, the {@link Error}'s own message will be further expected to
157
+ * either match the specified {@link RegExp}, or equal the specified `string`.
158
+ */
159
+ toBeError<Class extends Constructor<Error>>(
160
+ constructor: Class,
161
+ message?: string | RegExp,
162
+ ): Expectations<InstanceType<Class>>
163
+
164
+ toBeError(
165
+ constructorOrMessage?: string | RegExp | Constructor,
166
+ maybeMessage?: string | RegExp,
167
+ ): Expectations {
168
+ const [ constructor, message ] =
169
+ typeof constructorOrMessage === 'function' ?
170
+ [ constructorOrMessage, maybeMessage ] :
171
+ [ Error, constructorOrMessage ]
172
+
173
+ if (message === undefined) return this.toBeInstanceOf(constructor)
174
+
175
+ return this.toBeInstanceOf(constructor, (assert) => {
176
+ assert.toHaveProperty('message', (assertMessage) => {
177
+ assertMessage.toBeA('string')
178
+ if (typeof message === 'string') assertMessage.toStrictlyEqual(message)
179
+ else assertMessage.toMatch(message)
180
+ })
181
+ })
182
+ }
183
+
184
+ /* ------------------------------------------------------------------------ */
185
+
186
+ /** Expects the value strictly equal to `false`. */
187
+ toBeFalse(): Expectations<false> {
188
+ return this.toStrictlyEqual(false)
189
+ }
190
+
191
+ /* ------------------------------------------------------------------------ */
192
+
193
+ /**
194
+ * Expects the value to be _falsy_ (zero, empty string, `false`, ...).
195
+ *
196
+ * Negation: {@link Expectations.toBeTruthy `toBeTruthy()`}
197
+ */
198
+ toBeFalsy(): Expectations<T> {
199
+ if (! this.value) return this
200
+ this._fail('to be falsy')
201
+ }
202
+
203
+ /* ------------------------------------------------------------------------ */
204
+
205
+ /**
206
+ * Expects the value to be a `number` greater than the specified expected
207
+ * value.
208
+ *
209
+ * Negation: {@link Expectations.toBeLessThanOrEqual `toBeLessThanOrEqual(...)`}
210
+ */
211
+ toBeGreaterThan(value: number): Expectations<number>
212
+
213
+ /**
214
+ * Expects the value to be a `bigint` greater than the specified expected
215
+ * value.
216
+ *
217
+ * Negation: {@link Expectations.toBeLessThanOrEqual `toBeLessThanOrEqual(...)`}
218
+ */
219
+ toBeGreaterThan(value: bigint): Expectations<bigint>
220
+
221
+ toBeGreaterThan(value: number | bigint): Expectations {
222
+ this.toBeA(typeof value)
223
+ if ((this.value as any) > value) return this
224
+ this._fail(`to be greater than ${stringifyValue(value)}`)
225
+ }
226
+
227
+ /* ------------------------------------------------------------------------ */
228
+
229
+ /**
230
+ * Expects the value to be a `number` greater than or equal to the specified
231
+ * expected value.
232
+ *
233
+ * Negation: {@link Expectations.toBeLessThan `toBeLessThan(...)`}
234
+ */
235
+ toBeGreaterThanOrEqual(value: number): Expectations<number>
236
+
237
+ /**
238
+ * Expects the value to be a `bigint` greater than or equal to the specified
239
+ * expected value.
240
+ *
241
+ * Negation: {@link Expectations.toBeLessThan `toBeLessThan(...)`}
242
+ */
243
+ toBeGreaterThanOrEqual(value: bigint): Expectations<bigint>
244
+
245
+ toBeGreaterThanOrEqual(value: number | bigint): Expectations {
246
+ this.toBeA(typeof value)
247
+ if ((this.value as any) >= value) return this
248
+ this._fail(`to be greater than or equal to ${stringifyValue(value)}`)
249
+ }
250
+
251
+ /* ------------------------------------------------------------------------ */
252
+
253
+ /**
254
+ * Expects the value to be an instance of the specified {@link Constructor},
255
+ * and (if specified) further asserts it with an {@link AssertionFunction}.
256
+ *
257
+ * Negation: {@link NegativeExpectations.toBeInstanceOf `not.toInstanceOf(...)`}
258
+ */
259
+ toBeInstanceOf<
260
+ Class extends Constructor,
261
+ Assert extends AssertionFunction<InstanceType<Class>>,
262
+ >(
263
+ constructor: Class,
264
+ assertion?: Assert,
265
+ ): Expectations<AssertedType<InstanceType<Class>, Assert>> {
266
+ if (this.value instanceof constructor) {
267
+ if (assertion) assertion(this as Expectations<any>)
268
+ return this as Expectations<any>
269
+ }
270
+
271
+ this._fail(`to be an instance of ${stringifyConstructor(constructor)}`)
272
+ }
273
+
274
+ /* ------------------------------------------------------------------------ */
275
+
276
+ /**
277
+ * Expects the value to be a `number` less than the specified expected value.
278
+ *
279
+ * Negation: {@link Expectations.toBeGreaterThanOrEqual `toBeGreaterThanOrEqual(...)`}
280
+ */
281
+ toBeLessThan(value: number): Expectations<number>
282
+
283
+ /**
284
+ * Expects the value to be a `bigint` less than the specified expected value.
285
+ *
286
+ * Negation: {@link Expectations.toBeGreaterThanOrEqual `toBeGreaterThanOrEqual(...)`}
287
+ */
288
+ toBeLessThan(value: bigint): Expectations<bigint>
289
+
290
+ toBeLessThan(value: number | bigint): Expectations {
291
+ this.toBeA(typeof value)
292
+ if ((this.value as any) < value) return this
293
+ this._fail(`to be less than ${stringifyValue(value)}`)
294
+ }
295
+
296
+ /* ------------------------------------------------------------------------ */
297
+
298
+ /**
299
+ * Expects the value to be a `number` less than or equal to the specified
300
+ * expected value.
301
+ *
302
+ * Negation: {@link Expectations.toBeGreaterThan `toBeGreaterThan(...)`}
303
+ */
304
+ toBeLessThanOrEqual(value: number): Expectations<number>
305
+
306
+ /**
307
+ * Expects the value to be a `bigint` less than or equal to the specified
308
+ * expected value.
309
+ *
310
+ * Negation: {@link Expectations.toBeGreaterThan `toBeGreaterThan(...)`}
311
+ */
312
+ toBeLessThanOrEqual(value: bigint): Expectations<bigint>
313
+
314
+ toBeLessThanOrEqual(value: number | bigint): Expectations {
315
+ this.toBeA(typeof value)
316
+ if ((this.value as any) <= value) return this
317
+ this._fail(`to be less than or equal to ${stringifyValue(value)}`)
318
+ }
319
+
320
+ /* ------------------------------------------------------------------------ */
321
+
322
+ /**
323
+ * Expects the value to be `NaN`.
324
+ *
325
+ * Negation: {@link NegativeExpectations.toBeNaN `not.toBeNaN()`}
326
+ */
327
+ toBeNaN(): Expectations<number> {
328
+ const expectations = this.toBeA('number')
329
+ if (isNaN(expectations.value)) return expectations
330
+ this._fail(`to be ${stringifyValue(NaN)}`)
331
+ }
332
+
333
+ /* ------------------------------------------------------------------------ */
334
+
335
+ /** Expects the value to strictly equal `null`. */
336
+ toBeNull(): Expectations<null> {
337
+ return this.toStrictlyEqual(null)
338
+ }
339
+
340
+ /* ------------------------------------------------------------------------ */
341
+
342
+ /** Expects the value to strictly equal `true`. */
343
+ toBeTrue(): Expectations<true> {
344
+ return this.toStrictlyEqual(true)
345
+ }
346
+
347
+ /* ------------------------------------------------------------------------ */
348
+
349
+ /**
350
+ * Expects the value to be _falsy_ (non-zero, non-empty string, ...).
351
+ *
352
+ * Negation: {@link Expectations.toBeFalsy `toBeFalsy()`}
353
+ */
354
+ toBeTruthy(): Expectations<T> {
355
+ if (this.value) return this
356
+ this._fail('to be truthy')
357
+ }
358
+
359
+ /* ------------------------------------------------------------------------ */
360
+
361
+ /** Expects the value to strictly equal `undefined`. */
362
+ toBeUndefined(): Expectations<undefined> {
363
+ return this.toStrictlyEqual(undefined)
364
+ }
365
+
366
+ /* ------------------------------------------------------------------------ */
367
+
368
+ /**
369
+ * Expects the value to be a `number` within the specified range where the
370
+ * minimum and maximum values are inclusive.
371
+ *
372
+ * Negation: {@link NegativeExpectations.toBeWithinRange `not.toBeWithinRange(...)`}
373
+ */
374
+ toBeWithinRange(min: number, max: number): Expectations<number>
375
+
376
+ /**
377
+ * Expects the value to be a `bigint` within the specified range where the
378
+ * minimum and maximum values are inclusive.
379
+ *
380
+ * Negation: {@link NegativeExpectations.toBeWithinRange `not.toBeWithinRange(...)`}
381
+ */
382
+ toBeWithinRange(min: bigint, max: bigint): Expectations<bigint>
383
+
384
+ toBeWithinRange(min: number | bigint, max: number | bigint): Expectations {
385
+ if (max < min) {
386
+ const num = max
387
+ max = min
388
+ min = num
389
+ }
390
+
391
+ this.toBeA(typeof min)
392
+
393
+ if (((this.value as any) < min) || ((this.value as any) > max)) {
394
+ this._fail(`to be within ${stringifyValue(min)}...${stringifyValue(max)}`)
395
+ }
396
+
397
+ return this
398
+ }
399
+
400
+ /* ------------------------------------------------------------------------ */
401
+
402
+ /**
403
+ * Expects the value to be _deep equal to_ the specified expected one.
404
+ *
405
+ * Negation: {@link NegativeExpectations.toEqual `not.toEqual(...)`}
406
+ */
407
+ toEqual<Type>(expected: Type): Expectations<InferMatchers<Type>> {
408
+ if ((this.value as any) === expected) return this as Expectations<any>
409
+
410
+ const result = diff(this.value, expected)
411
+
412
+ if (result.diff) {
413
+ if (isMatcher(expected)) {
414
+ this._fail('to satisfy expectation matcher', result)
415
+ } else {
416
+ this._fail(`to equal ${stringifyValue(expected)}`, result)
417
+ }
418
+ } else {
419
+ return this as Expectations<any>
420
+ }
421
+ }
422
+
423
+ /* ------------------------------------------------------------------------ */
424
+
425
+ /**
426
+ * Expects the value to have a `number` _property_ `length` with the specified
427
+ * expected value.
428
+ *
429
+ * Negation: {@link NegativeExpectations.toHaveLength `not.toHaveLength(...)`}
430
+ */
431
+ toHaveLength(length: number): Expectations<T & { length: number }> {
432
+ this.toBeDefined()
433
+
434
+ const realLength = (this.value as any)['length']
435
+ if (typeof realLength !== 'number') {
436
+ this._fail('to have a numeric "length" property')
437
+ } else if (realLength !== length) {
438
+ this._fail(`to have length ${length}`)
439
+ }
440
+
441
+ return this as Expectations<any>
442
+ }
443
+
444
+ /* ------------------------------------------------------------------------ */
445
+
446
+ /**
447
+ * Expects the value to have the specified _property_ and (if specified)
448
+ * further asserts its value with an {@link AssertionFunction}.
449
+ *
450
+ * Negation: {@link NegativeExpectations.toHaveProperty `not.toHaveProperty(...)`}
451
+ */
452
+ toHaveProperty<
453
+ Prop extends string | number | symbol,
454
+ Assert extends AssertionFunction,
455
+ >(
456
+ property: Prop,
457
+ assertion?: Assert,
458
+ ): Expectations<T & { [keyt in Prop] : AssertedType<unknown, Assert> }> {
459
+ this.toBeDefined()
460
+
461
+ const propertyValue = (this.value as any)[property]
462
+
463
+ if (propertyValue === undefined) {
464
+ this._fail(`to have property "${String(property)}"`)
465
+ }
466
+
467
+ if (assertion) {
468
+ try {
469
+ const parent: ExpectationsParent = { expectations: this, prop: property }
470
+ const expectations = new Expectations(propertyValue, this.remarks, parent)
471
+ assertion(expectations)
472
+ } catch (error) {
473
+ // any caught error difference gets remapped as a property diff
474
+ if ((error instanceof ExpectationError) && (error.diff)) {
475
+ error.diff = {
476
+ diff: true,
477
+ value: this.value,
478
+ props: { [property]: error.diff },
479
+ }
480
+ }
481
+
482
+ // re-throw
483
+ throw error
484
+ }
485
+ }
486
+ return this as Expectations<any>
487
+ }
488
+
489
+ /* ------------------------------------------------------------------------ */
490
+
491
+ /**
492
+ * Expects the value to have a `number` _property_ `size` with the specified
493
+ * expected value.
494
+ *
495
+ * Negation: {@link NegativeExpectations.toHaveSize `not.toHaveSize(...)`}
496
+ */
497
+ toHaveSize(size: number): Expectations<T & { size: number }> {
498
+ this.toBeDefined()
499
+
500
+ const realSize = (this.value as any)['size']
501
+ if (typeof realSize !== 'number') {
502
+ this._fail('to have a numeric "size" property')
503
+ } else if (realSize !== size) {
504
+ this._fail(`to have size ${size}`)
505
+ }
506
+
507
+ return this as Expectations<any>
508
+ }
509
+
510
+ /* ------------------------------------------------------------------------ */
511
+
512
+ /**
513
+ * Expect the value to include _all_ properties from the specified _object_.
514
+ *
515
+ * If the object being expected is a {@link Map}, the properties specified
516
+ * here will be treated as _mappings_ for said {@link Map}.
517
+ *
518
+ * Negation: {@link NegativeExpectations.toInclude `not.toInclude(...)`}
519
+ */
520
+ toInclude<P extends Record<string, any>>(properties: P): Expectations<T>
521
+
522
+ /**
523
+ * Expect the value to include _all_ mappings from the specified {@link Map}.
524
+ *
525
+ * Negation: {@link NegativeExpectations.toInclude `not.toInclude(...)`}
526
+ */
527
+ toInclude(mappings: Map<any, any>): Expectations<T>
528
+
529
+ /**
530
+ * Expect the value to be an {@link Iterable} object includind _all_ values
531
+ * from the specified {@link Set}, in any order.
532
+ *
533
+ * Negation: {@link NegativeExpectations.toInclude `not.toInclude(...)`}
534
+ */
535
+ toInclude(entries: Set<any>): Expectations<T>
536
+
537
+ /**
538
+ * Expect the value to be an {@link Iterable} object includind _all_ values
539
+ * from the specified _array_, in any order.
540
+ *
541
+ * Negation: {@link NegativeExpectations.toInclude `not.toInclude(...)`}
542
+ */
543
+ toInclude(values: any[]): Expectations<T>
544
+
545
+ toInclude(
546
+ contents: Record<string, any> | Map<any, any> | Set<any> | any[],
547
+ ): Expectations {
548
+ toInclude(this, false, contents)
549
+ return this
550
+ }
551
+
552
+ /* ------------------------------------------------------------------------ */
553
+
554
+ /**
555
+ * Expects the value to be a `string` _matching_ the specified sub-`string`
556
+ * or {@link RegExp}.
557
+ *
558
+ * Negation: {@link NegativeExpectations.toMatch `not.toMatch(...)`}
559
+ */
560
+ toMatch<Matcher extends string | RegExp>(
561
+ matcher: Matcher,
562
+ ): Expectations<string> {
563
+ const expectations = this.toBeA('string')
564
+
565
+ if (expectations.value.match(matcher)) return expectations
566
+
567
+ this._fail(`to match ${stringifyValue(matcher)}`)
568
+ }
569
+
570
+ /* ------------------------------------------------------------------------ */
571
+
572
+ /**
573
+ * Expect the value to be an {@link Iterable} object includind _all_ values
574
+ * (and only those values) from the specified _array_ or {@link Set},
575
+ * in any order.
576
+ */
577
+ toMatchContents(contents: any[] | Set<any>): Expectations<T> {
578
+ toMatchContents(this, contents)
579
+ return this
580
+ }
581
+
582
+ /* ------------------------------------------------------------------------ */
583
+
584
+ /**
585
+ * Expects the value to be _strictly equal to_ the specified expected one.
586
+ *
587
+ * Negation: {@link NegativeExpectations.toStrictlyEqual `not.toStrictlyEqual(...)`}
588
+ */
589
+ toStrictlyEqual<Type>(expected: Type): Expectations<Type> {
590
+ if ((this.value as any) === expected) return this as Expectations<any>
591
+
592
+ const diff = { diff: true, value: this.value, expected }
593
+ this._fail(`to strictly equal ${stringifyValue(expected)}`, diff)
594
+ }
595
+
596
+ /* ------------------------------------------------------------------------ */
597
+
598
+ /**
599
+ * Expects the value to be a `function` throwing, and (if specified) further
600
+ * asserts the thrown value with an {@link AssertionFunction}.
601
+ *
602
+ * Negation: {@link NegativeExpectations.toThrow `not.toThrow()`}
603
+ */
604
+ toThrow(assert?: AssertionFunction): Expectations<() => any> {
605
+ const func = this.toBeA('function')
606
+
607
+ let passed = false
608
+ try {
609
+ func.value()
610
+ passed = true
611
+ } catch (thrown) {
612
+ if (assert) assert(new Expectations(thrown, this.remarks))
613
+ }
614
+
615
+ if (passed) this._fail('to throw')
616
+ return this as Expectations<any>
617
+ }
618
+
619
+ /* ------------------------------------------------------------------------ */
620
+
621
+ /**
622
+ * Expects the value to be a `function` throwing an {@link Error}.
623
+ *
624
+ * If specified, the {@link Error}'s own message will be further expected to
625
+ * either match the specified {@link RegExp}, or equal to the specified
626
+ * `string`.
627
+ *
628
+ * Negation: {@link NegativeExpectations.toThrow `not.toThrow()`}
629
+ */
630
+ toThrowError(
631
+ message?: string | RegExp
632
+ ): Expectations<() => any>
633
+
634
+ /**
635
+ * Expects the value to be a `function` throwing an instance of the
636
+ * {@link Error} identified by the specified {@link Constructor}.
637
+ *
638
+ * If specified, the {@link Error}'s own message will be further expected to
639
+ * either match the specified {@link RegExp}, or equal to the specified
640
+ * `string`.
641
+ *
642
+ * Negation: {@link NegativeExpectations.toThrow `not.toThrow()`}
643
+ */
644
+ toThrowError<Class extends Constructor<Error>>(
645
+ constructor: Class,
646
+ message?: string | RegExp,
647
+ ): Expectations<() => any>
648
+
649
+ toThrowError(
650
+ constructorOrMessage?: string | RegExp | Constructor,
651
+ maybeMessage?: string | RegExp,
652
+ ): Expectations {
653
+ const [ constructor, message ] =
654
+ typeof constructorOrMessage === 'function' ?
655
+ [ constructorOrMessage, maybeMessage ] :
656
+ [ Error, constructorOrMessage ]
657
+
658
+ return this.toThrow((assert) =>
659
+ assert.toBeError(constructor, message))
660
+ }
661
+ }
662
+
663
+ /* ========================================================================== *
664
+ * NEGATIVE EXPECTATIONS *
665
+ * ========================================================================== */
666
+
667
+ /** Negative expectations, as a subset of (meaningful) expectations. */
668
+ export class NegativeExpectations<T = unknown> {
669
+ /** For convenience, the value associated with the {@link Expectations} */
670
+ private readonly _value: T
671
+
672
+ /**
673
+ * Create a {@link NegativeExpectations} instance associated with the
674
+ * specified (positive) {@link Expectations}.
675
+ */
676
+ constructor(
677
+ /** The {@link Expectations} instance associated with this one */
678
+ private readonly _expectations: Expectations<T>,
679
+ ) {
680
+ this._value = _expectations.value
681
+ }
682
+
683
+ /** Throw an {@link ExpectationError} associated with _this_ */
684
+ private _fail(details: string, diff?: Diff): never {
685
+ throw new ExpectationError(this._expectations, details, diff)
686
+ }
687
+
688
+ /* ------------------------------------------------------------------------ */
689
+
690
+ /**
691
+ * Expects the value _**NOT**_ to be of the specified _extended_
692
+ * {@link TypeName type}.
693
+ *
694
+ * Negates: {@link Expectations.toBeA `toBeA(...)`}
695
+ */
696
+ toBeA(type: TypeName): Expectations<T> {
697
+ if (typeOf(this._value) !== type) return this._expectations
698
+ this._fail(`not to be ${prefixType(type)}`)
699
+ }
700
+
701
+ /* ------------------------------------------------------------------------ */
702
+
703
+ /**
704
+ * Expects the value to be a `number` _**OUTSIDE**_ of the given +/- _delta_
705
+ * range of the specified expected value.
706
+ *
707
+ * Negates: {@link Expectations.toBeCloseTo `toBeCloseTo(...)`}
708
+ */
709
+ toBeCloseTo(value: number, delta: number): Expectations<number>
710
+
711
+ /**
712
+ * Expects the value to be a `bigint` _**OUTSIDE**_ of the given +/- _delta_
713
+ * range of the specified expected value.
714
+ *
715
+ * Negates: {@link Expectations.toBeCloseTo `toBeCloseTo(...)`}
716
+ */
717
+ toBeCloseTo(value: bigint, delta: bigint): Expectations<bigint>
718
+
719
+ toBeCloseTo(value: number | bigint, delta: number | bigint): Expectations {
720
+ const min = (value as number) - (delta as number)
721
+ const max = (value as number) + (delta as number)
722
+ return this.toBeWithinRange(min, max)
723
+ }
724
+
725
+ /* ------------------------------------------------------------------------ */
726
+
727
+ /**
728
+ * Expects the value to be either `null` or `undefined`.
729
+ *
730
+ * Negates: {@link Expectations.toBeDefined `toBeDefined()`}
731
+ */
732
+ toBeDefined(): Expectations<null | undefined> {
733
+ if ((this._value === null) || (this._value === undefined)) {
734
+ return this._expectations as Expectations<any>
735
+ }
736
+
737
+ this._fail(`to be ${stringifyValue(null)} or ${stringifyValue(undefined)}`)
738
+ }
739
+
740
+ /* ------------------------------------------------------------------------ */
741
+
742
+ /**
743
+ * Expects the value _**NOT**_ to be an instance of the specified
744
+ * {@link Constructor}.
745
+ *
746
+ * Negates: {@link Expectations.toBeInstanceOf `toBeInstanceOf(...)`}
747
+ */
748
+ toBeInstanceOf(constructor: Constructor): Expectations<T> {
749
+ if (this._value instanceof constructor) {
750
+ this._fail(`not to be an instance of ${stringifyConstructor(constructor)}`)
751
+ }
752
+ return this._expectations
753
+ }
754
+
755
+ /* ------------------------------------------------------------------------ */
756
+
757
+ /**
758
+ * Expects the value _**NOT**_ to be `NaN`.
759
+ *
760
+ * Negates: {@link Expectations.toBeNaN `toBeNaN()`}
761
+ */
762
+ toBeNaN(): Expectations<number> {
763
+ const expectations = this._expectations.toBeA('number')
764
+ if (isNaN(expectations.value)) this._fail(`not to be ${stringifyValue(NaN)}`)
765
+ return expectations
766
+ }
767
+
768
+ /* ------------------------------------------------------------------------ */
769
+
770
+ /**
771
+ * Expects the value to be a `number` _**OUTSIDE**_ of the specified range
772
+ * where minimum and maximum values are inclusive.
773
+ *
774
+ * Negates: {@link Expectations.toBeWithinRange `toBeWithinRange(...)`}
775
+ */
776
+ toBeWithinRange(min: number, max: number): Expectations<number>
777
+
778
+ /**
779
+ * Expects the value to be a `bigint` _**OUTSIDE**_ of the specified range
780
+ * where minimum and maximum values are inclusive.
781
+ *
782
+ * Negates: {@link Expectations.toBeWithinRange `toBeWithinRange(...)`}
783
+ */
784
+ toBeWithinRange(min: bigint, max: bigint): Expectations<bigint>
785
+
786
+ toBeWithinRange(min: number | bigint, max: number | bigint): Expectations {
787
+ if (max < min) {
788
+ const num = max
789
+ max = min
790
+ min = num
791
+ }
792
+
793
+ this._expectations.toBeA(typeof min)
794
+
795
+ if (((this._value as any) >= min) && ((this._value as any) <= max)) {
796
+ this._fail(`not to be within ${stringifyValue(min)}...${stringifyValue(max)}`)
797
+ }
798
+
799
+ return this._expectations
800
+ }
801
+
802
+ /* ------------------------------------------------------------------------ */
803
+
804
+ /**
805
+ * Expects the value _**NOT**_ to be _deep equal to_ the specified expected
806
+ * one.
807
+ *
808
+ * Negates: {@link Expectations.toEqual `toEqual(...)`}
809
+ */
810
+ toEqual(expected: any): Expectations<T> {
811
+ let result: Diff = { diff: false, value: this._value }
812
+ if (this._value !== expected) {
813
+ result = diff(this._value, expected)
814
+ if (result.diff) return this._expectations
815
+ }
816
+
817
+ if (isMatcher(expected)) {
818
+ this._fail('not to satisfy expectation matcher', result)
819
+ } else {
820
+ this._fail(`not to equal ${stringifyValue(expected)}`, result)
821
+ }
822
+ }
823
+
824
+ /* ------------------------------------------------------------------------ */
825
+
826
+ /**
827
+ * Expects the value to have a `number` _property_ `length` _different_ from
828
+ * the specified expected value.
829
+ *
830
+ * Negates: {@link Expectations.toHaveLength `toHaveLength(...)`}
831
+ */
832
+ toHaveLength(length: number): Expectations<T & { length: number }> {
833
+ this._expectations.toBeDefined()
834
+
835
+ const realLength = (this._value as any)['length']
836
+ if (typeof realLength !== 'number') {
837
+ this._fail('to have a numeric "length" property')
838
+ } else if (realLength === length) {
839
+ this._fail(`not to have length ${length}`)
840
+ }
841
+
842
+ return this._expectations as Expectations<any>
843
+ }
844
+
845
+ /* ------------------------------------------------------------------------ */
846
+
847
+ /**
848
+ * Expects the value _**NOT**_ to have the specified _property_.
849
+ *
850
+ * Negates: {@link Expectations.toHaveProperty `toHaveProperty(...)`}
851
+ */
852
+ toHaveProperty(property: string | number | symbol): Expectations<T> {
853
+ this._expectations.toBeDefined()
854
+
855
+ const propertyValue = (this._value as any)[property]
856
+ if (propertyValue === undefined) return this._expectations
857
+ this._fail(`not to have property "${String(property)}"`)
858
+ }
859
+
860
+ /* ------------------------------------------------------------------------ */
861
+
862
+ /**
863
+ * Expects the value to have a `number` _property_ `size` _different_ from
864
+ * the specified expected value.
865
+ *
866
+ * Negates: {@link Expectations.toHaveSize `toHaveSize(...)`}
867
+ */
868
+ toHaveSize(size: number): Expectations<T & { size: number }> {
869
+ this._expectations.toBeDefined()
870
+
871
+ const realSize = (this._value as any)['size']
872
+ if (typeof realSize !== 'number') {
873
+ this._fail('to have a numeric "size" property')
874
+ } else if (realSize === size) {
875
+ this._fail(`not to have size ${size}`)
876
+ }
877
+
878
+ return this._expectations as Expectations<any>
879
+ }
880
+
881
+ /* ------------------------------------------------------------------------ */
882
+
883
+ /**
884
+ * Expect the value to include _none_ of the properties from the specified
885
+ * _object_.
886
+ *
887
+ * If the object being expected is a {@link Map}, the properties specified
888
+ * here will be treated as _mappings_ for said {@link Map}.
889
+ *
890
+ * Negates: {@link Expectations.toInclude `toInclude(...)`}
891
+ */
892
+ toInclude<P extends Record<string, any>>(properties: P): Expectations<T>
893
+
894
+ /**
895
+ * Expect the value to include _none_ of the mappings from the specified
896
+ * {@link Map}.
897
+ *
898
+ * Negates: {@link Expectations.toInclude `toInclude(...)`}
899
+ */
900
+ toInclude(mappings: Map<any, any>): Expectations<T>
901
+
902
+ /**
903
+ * Expect the value to be an {@link Iterable} object includind _none_ of the
904
+ * values from the specified {@link Set}.
905
+ *
906
+ * Negates: {@link Expectations.toInclude `toInclude(...)`}
907
+ */
908
+ toInclude(entries: Set<any>): Expectations<T>
909
+
910
+ /**
911
+ * Expect the value to be an {@link Iterable} object includind _none_ of the
912
+ * values from the specified _array_.
913
+ *
914
+ * Negates: {@link Expectations.toInclude `toInclude(...)`}
915
+ */
916
+ toInclude(values: any[]): Expectations<T>
917
+
918
+ toInclude(
919
+ contents: Record<string, any> | Map<any, any> | Set<any> | any[],
920
+ ): Expectations {
921
+ toInclude(this._expectations, true, contents)
922
+ return this._expectations
923
+ }
924
+
925
+ /* ------------------------------------------------------------------------ */
926
+
927
+ /**
928
+ * Expects the value to be a `string` _**NOT MATCHING**_ the specified
929
+ * sub-`string` or {@link RegExp}.
930
+ *
931
+ * Negates: {@link Expectations.toMatch `toMatch(...)`}
932
+ */
933
+ toMatch(matcher: string | RegExp): Expectations<string> {
934
+ const expectations = this._expectations.toBeA('string')
935
+
936
+ if (! expectations.value.match(matcher)) return expectations
937
+
938
+ this._fail(`not to match ${stringifyValue(matcher)}`)
939
+ }
940
+
941
+ /* ------------------------------------------------------------------------ */
942
+
943
+ /**
944
+ * Expects the value to be a `function` not throwing anything.
945
+ *
946
+ * Negates: {@link Expectations.toThrow `toThrow(...)`}
947
+ */
948
+ toThrow(): Expectations<() => any> {
949
+ const expectations = this._expectations.toBeA('function')
950
+
951
+ try {
952
+ expectations.value()
953
+ return expectations
954
+ } catch (caught) {
955
+ this._fail('not to throw')
956
+ }
957
+ }
958
+
959
+ /* ------------------------------------------------------------------------ */
960
+
961
+ /**
962
+ * Expects the value _**NOT**_ to be _strictly equal to_ the specified
963
+ * expected one.
964
+ *
965
+ * Negates: {@link Expectations.toStrictlyEqual `toStrictlyEqual(...)`}
966
+ */
967
+ toStrictlyEqual(expected: any): Expectations<T> {
968
+ if (this._value !== expected) return this._expectations
969
+ this._fail(`not to strictly equal ${stringifyValue(expected)}`)
970
+ }
971
+ }