@plugjs/expect5 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.d.mts +2 -0
  3. package/dist/cli.mjs +96 -0
  4. package/dist/cli.mjs.map +6 -0
  5. package/dist/execution/executable.cjs +299 -0
  6. package/dist/execution/executable.cjs.map +6 -0
  7. package/dist/execution/executable.d.ts +87 -0
  8. package/dist/execution/executable.mjs +260 -0
  9. package/dist/execution/executable.mjs.map +6 -0
  10. package/dist/execution/executor.cjs +125 -0
  11. package/dist/execution/executor.cjs.map +6 -0
  12. package/dist/execution/executor.d.ts +35 -0
  13. package/dist/execution/executor.mjs +90 -0
  14. package/dist/execution/executor.mjs.map +6 -0
  15. package/dist/execution/setup.cjs +127 -0
  16. package/dist/execution/setup.cjs.map +6 -0
  17. package/dist/execution/setup.d.ts +31 -0
  18. package/dist/execution/setup.mjs +87 -0
  19. package/dist/execution/setup.mjs.map +6 -0
  20. package/dist/expectation/basic.cjs +216 -0
  21. package/dist/expectation/basic.cjs.map +6 -0
  22. package/dist/expectation/basic.d.ts +47 -0
  23. package/dist/expectation/basic.mjs +177 -0
  24. package/dist/expectation/basic.mjs.map +6 -0
  25. package/dist/expectation/diff.cjs +253 -0
  26. package/dist/expectation/diff.cjs.map +6 -0
  27. package/dist/expectation/diff.d.ts +27 -0
  28. package/dist/expectation/diff.mjs +228 -0
  29. package/dist/expectation/diff.mjs.map +6 -0
  30. package/dist/expectation/expect.cjs +211 -0
  31. package/dist/expectation/expect.cjs.map +6 -0
  32. package/dist/expectation/expect.d.ts +140 -0
  33. package/dist/expectation/expect.mjs +219 -0
  34. package/dist/expectation/expect.mjs.map +6 -0
  35. package/dist/expectation/include.cjs +187 -0
  36. package/dist/expectation/include.cjs.map +6 -0
  37. package/dist/expectation/include.d.ts +10 -0
  38. package/dist/expectation/include.mjs +158 -0
  39. package/dist/expectation/include.mjs.map +6 -0
  40. package/dist/expectation/print.cjs +281 -0
  41. package/dist/expectation/print.cjs.map +6 -0
  42. package/dist/expectation/print.d.ts +4 -0
  43. package/dist/expectation/print.mjs +256 -0
  44. package/dist/expectation/print.mjs.map +6 -0
  45. package/dist/expectation/throwing.cjs +58 -0
  46. package/dist/expectation/throwing.cjs.map +6 -0
  47. package/dist/expectation/throwing.d.ts +8 -0
  48. package/dist/expectation/throwing.mjs +32 -0
  49. package/dist/expectation/throwing.mjs.map +6 -0
  50. package/dist/expectation/types.cjs +212 -0
  51. package/dist/expectation/types.cjs.map +6 -0
  52. package/dist/expectation/types.d.ts +57 -0
  53. package/dist/expectation/types.mjs +178 -0
  54. package/dist/expectation/types.mjs.map +6 -0
  55. package/dist/expectation/void.cjs +111 -0
  56. package/dist/expectation/void.cjs.map +6 -0
  57. package/dist/expectation/void.d.ts +39 -0
  58. package/dist/expectation/void.mjs +77 -0
  59. package/dist/expectation/void.mjs.map +6 -0
  60. package/dist/globals.cjs +2 -0
  61. package/dist/globals.cjs.map +6 -0
  62. package/dist/globals.d.ts +23 -0
  63. package/dist/globals.mjs +1 -0
  64. package/dist/globals.mjs.map +6 -0
  65. package/dist/index.cjs +66 -0
  66. package/dist/index.cjs.map +6 -0
  67. package/dist/index.d.ts +29 -0
  68. package/dist/index.mjs +41 -0
  69. package/dist/index.mjs.map +6 -0
  70. package/dist/test.cjs +229 -0
  71. package/dist/test.cjs.map +6 -0
  72. package/dist/test.d.ts +9 -0
  73. package/dist/test.mjs +194 -0
  74. package/dist/test.mjs.map +6 -0
  75. package/package.json +57 -0
  76. package/src/cli.mts +122 -0
  77. package/src/execution/executable.ts +364 -0
  78. package/src/execution/executor.ts +146 -0
  79. package/src/execution/setup.ts +108 -0
  80. package/src/expectation/basic.ts +209 -0
  81. package/src/expectation/diff.ts +445 -0
  82. package/src/expectation/expect.ts +401 -0
  83. package/src/expectation/include.ts +184 -0
  84. package/src/expectation/print.ts +386 -0
  85. package/src/expectation/throwing.ts +45 -0
  86. package/src/expectation/types.ts +263 -0
  87. package/src/expectation/void.ts +80 -0
  88. package/src/globals.ts +30 -0
  89. package/src/index.ts +54 -0
  90. package/src/test.ts +239 -0
@@ -0,0 +1,386 @@
1
+ import { $grn, $gry, $red, $und, $wht, $ylw } from '@plugjs/plug/logging'
2
+ import { textDiff } from '@plugjs/plug/utils'
3
+
4
+ import { stringifyValue } from './types'
5
+
6
+ import type { Diff, ExtraValueDiff, MissingValueDiff, ObjectDiff, ExpectedDiff } from './diff'
7
+ import type { Logger } from '@plugjs/plug/logging'
8
+
9
+ /* ========================================================================== *
10
+ * CONSTANT LABELS FOR PRINTING *
11
+ * ========================================================================== */
12
+
13
+ const _opnPar = $gry('(')
14
+ const _clsPar = $gry(')')
15
+
16
+ const _opnCrl = $gry('{')
17
+ const _clsCrl = $gry('}')
18
+ const _curls = $gry('{}')
19
+
20
+ const _opnSqr = $gry('[')
21
+ const _clsSqr = $gry(']')
22
+ const _squares = $gry('[]')
23
+
24
+ const _slash = $gry('/')
25
+ const _tilde = $gry('~')
26
+ const _hellip = $gry('\u2026')
27
+
28
+ const _error = `${_opnPar}${$gry($und('error'))}${_clsPar}`
29
+ const _string = `${_opnPar}${$gry($und('string'))}${_clsPar}`
30
+ const _extraProps = $gry('\u2026 extra props \u2026')
31
+ const _diffHeader = `${$wht('Differences')} ${_opnPar}${$red('actual')}${_slash}${$grn('expected')}${_slash}${$ylw('errors')}${_clsPar}:`
32
+
33
+ /* ========================================================================== *
34
+ * PRINT DEPENDING ON DIFF TYPE *
35
+ * ========================================================================== */
36
+
37
+ function printBaseDiff(
38
+ log: Logger,
39
+ diff: Diff,
40
+ prop: string,
41
+ mapping: boolean,
42
+ comma: boolean,
43
+ ): void {
44
+ if ('props' in diff) return printObjectDiff(log, diff, prop, mapping, comma)
45
+ if ('values' in diff) return printObjectDiff(log, diff, prop, mapping, comma)
46
+ if ('mappings' in diff) return printObjectDiff(log, diff, prop, mapping, comma)
47
+ if ('expected' in diff) return printExpectedDiff(log, diff, prop, mapping, comma)
48
+ if ('missing' in diff) return printMissingDiff(log, diff, prop, mapping, comma)
49
+ if ('extra' in diff) return printExtraDiff(log, diff, prop, mapping, comma)
50
+
51
+ const { prefix, suffix } =
52
+ diff.error ? // default style if error is the only property
53
+ fixups(prop, mapping, comma, diff.error) :
54
+ diff.diff ? // label as "differs" if no error was found
55
+ fixups(prop, mapping, comma, diff.error, $red, 'differs') :
56
+ fixups(prop, mapping, comma, diff.error)
57
+
58
+ dump(log, diff.value, prefix, suffix, diff.diff ? $red : $wht)
59
+ }
60
+
61
+ /* ========================================================================== */
62
+
63
+ function printExpectedDiff(
64
+ log: Logger,
65
+ diff: ExpectedDiff,
66
+ prop: string,
67
+ mapping: boolean,
68
+ comma: boolean,
69
+ ): void {
70
+ // two different strings get a special treatment: a proper "diff"
71
+ if ((typeof diff.value === 'string') && (typeof diff.expected === 'string')) {
72
+ const { prefix, suffix } = fixups(prop, mapping, false, diff.error)
73
+
74
+ log.warn(`${prefix}${_string}${suffix}`)
75
+ textDiff(diff.value, diff.expected).split('\n').forEach((line) => {
76
+ log.warn(` ${_hellip} ${line}`)
77
+ })
78
+
79
+ // if "value" is not an object (can fit on one line) we use it as prefix
80
+ } else if ((diff.value === null) || (typeof diff.value !== 'object')) {
81
+ const { prefix, suffix } = fixups(prop, mapping, comma, diff.error)
82
+
83
+ const joined = `${prefix}${$red(stringify(diff.value))} ${_tilde} `
84
+ dump(log, diff.expected, joined, suffix, $grn)
85
+
86
+ // if "expected" is not an object (can fit on one line) we use it as suffix
87
+ } else if ((diff.expected === null) || (typeof diff.expected !== 'object')) {
88
+ const { prefix, suffix } = fixups(prop, mapping, comma, diff.error)
89
+
90
+ const joined = ` ${_tilde} ${$grn(stringify(diff.expected))}${suffix}`
91
+ dump(log, diff.value, prefix, joined, $red)
92
+
93
+ // both "value" and "expected" are objects, so, we join them with a ~
94
+ } else {
95
+ // here the error _only_ goes on the last line...
96
+ const { prefix, suffix: suffix1 } = fixups(prop, mapping, false, '')
97
+ const { suffix: suffix2 } = fixups(prop, mapping, comma, diff.error)
98
+
99
+ const lastLine = dumpAndContinue(log, diff.expected, prefix, suffix1, $red)
100
+ dump(log, diff.value, `${lastLine} ${_tilde} `, suffix2, $grn)
101
+ }
102
+ }
103
+
104
+ /* ========================================================================== */
105
+
106
+ function printMissingDiff(
107
+ log: Logger,
108
+ diff: MissingValueDiff,
109
+ prop: string,
110
+ mapping: boolean,
111
+ comma: boolean,
112
+ ): void {
113
+ const { prefix, suffix } = fixups(prop, mapping, comma, diff.error, $red, 'missing')
114
+ dump(log, diff.missing, prefix, suffix, $red)
115
+ }
116
+
117
+ /* ========================================================================== */
118
+
119
+ function printExtraDiff(
120
+ log: Logger,
121
+ diff: ExtraValueDiff,
122
+ prop: string,
123
+ mapping: boolean,
124
+ comma: boolean,
125
+ ): void {
126
+ const { prefix, suffix } = fixups(prop, mapping, comma, diff.error, $red, 'extra')
127
+ dump(log, diff.extra, prefix, suffix, $red)
128
+ }
129
+
130
+ /* ========================================================================== */
131
+
132
+ function printObjectDiff(
133
+ log: Logger,
134
+ diff: ObjectDiff,
135
+ prop: string,
136
+ mapping: boolean,
137
+ comma: boolean,
138
+ ): void {
139
+ const { prefix, suffix } = fixups(prop, mapping, comma, diff.error)
140
+
141
+ // prepare for deep inspection
142
+ const value = diff.value
143
+ const ctor = Object.getPrototypeOf(value)?.constructor
144
+ const string = (ctor === Object) || (ctor === Array) ? '' : stringifyValue(value)
145
+
146
+ // prepare first line of output
147
+ let line = string ? `${prefix}${$wht(string)} ` : prefix
148
+ let marked = false
149
+
150
+ // arrays or sets
151
+ if (diff.values) {
152
+ if (diff.values.length === 0) {
153
+ line = `${line}${_squares}`
154
+ } else {
155
+ log.warn(`${line}${_opnSqr}`)
156
+ log.enter()
157
+ try {
158
+ for (const subdiff of diff.values) {
159
+ printBaseDiff(log, subdiff, '', false, true)
160
+ }
161
+ } finally {
162
+ log.leave()
163
+ }
164
+ line = _clsSqr
165
+ }
166
+ marked = true
167
+
168
+ // values and mappings (arrays/sets and maps) are mutually exclusive
169
+ } else if (diff.mappings) {
170
+ if (Object.keys(diff.mappings).length === 0) {
171
+ line = `${line}${_curls}`
172
+ } else {
173
+ log.warn(`${line}${_opnCrl}`)
174
+ log.enter()
175
+ try {
176
+ for (const [ key, subdiff ] of diff.mappings) {
177
+ printBaseDiff(log, subdiff, stringifyValue(key), true, true)
178
+ }
179
+ } finally {
180
+ log.leave()
181
+ }
182
+ line = _clsCrl
183
+ }
184
+ marked = true
185
+ }
186
+
187
+ // extra properties
188
+ if (diff.props) {
189
+ if (marked) line = `${line} ${_extraProps} `
190
+ if (Object.keys(diff.props).length === 0) {
191
+ line = `${line}${_curls}`
192
+ } else {
193
+ log.warn(`${line}${_opnCrl}`)
194
+ log.enter()
195
+ try {
196
+ for (const [ prop, subdiff ] of Object.entries(diff.props)) {
197
+ printBaseDiff(log, subdiff, stringifyValue(prop), false, true)
198
+ }
199
+ } finally {
200
+ log.leave()
201
+ }
202
+ line = _clsCrl
203
+ }
204
+ marked = true
205
+ }
206
+
207
+ log.warn(`${line}${suffix}`)
208
+ }
209
+
210
+ /* ========================================================================== *
211
+ * PRINT HELPERS *
212
+ * ========================================================================== */
213
+
214
+ function stringify(
215
+ value: null | undefined | string | number | boolean | bigint | symbol | Function,
216
+ ): string {
217
+ if (typeof value === 'string') return JSON.stringify(value)
218
+ return stringifyValue(value)
219
+ }
220
+
221
+ function fixups(
222
+ prop: string,
223
+ mapping: boolean,
224
+ comma: boolean,
225
+ error: string | undefined,
226
+ color?: ((string: string) => string) | undefined,
227
+ label?: string,
228
+ ): { prefix: string, suffix: string } {
229
+ if (error) color = color || $ylw
230
+
231
+ const lbl = label ? `${_opnPar}${$gry($und(label))}${_clsPar} ` : ''
232
+ const sep = mapping ? ' => ': ': '
233
+ const prefix = prop ?
234
+ color ?
235
+ `${$gry(lbl)}${color(prop)}${$gry(sep)}` :
236
+ `${$gry(lbl)}${prop}${$gry(sep)}` :
237
+ label ?
238
+ `${$gry(lbl)}` :
239
+ ''
240
+ error = error ? ` ${_error} ${$ylw(error)}` : ''
241
+ const suffix = `${comma ? $gry(',') : ''}${error}`
242
+ return { prefix, suffix }
243
+ }
244
+
245
+ function dump(
246
+ log: Logger,
247
+ value: any,
248
+ prefix: string,
249
+ suffix: string,
250
+ color: (string: string) => string,
251
+ stack: any[] = [],
252
+ ): void {
253
+ log.warn(dumpAndContinue(log, value, prefix, suffix, color, stack))
254
+ }
255
+
256
+ function dumpAndContinue(
257
+ log: Logger,
258
+ value: any,
259
+ prefix: string,
260
+ suffix: string,
261
+ color: (string: string) => string,
262
+ stack: any[] = [],
263
+ ): string {
264
+ // primitives just get dumped
265
+ if ((value === null) || (typeof value !== 'object')) {
266
+ return `${prefix}${color(stringify(value))}${suffix}`
267
+ }
268
+
269
+ // check for circular dependencies
270
+ const circular = stack.indexOf(value)
271
+ if (circular >= 0) {
272
+ return `${prefix}${$gry($und(`<circular ${circular}>`))}${suffix}`
273
+ }
274
+
275
+ // prepare for deep inspection
276
+ const ctor = Object.getPrototypeOf(value)?.constructor
277
+ const string = (ctor === Object) || (ctor === Array) ? '' : stringifyValue(value)
278
+ const keys = new Set(Object.keys(value))
279
+
280
+ // prepare first line of output
281
+ let line = string ? `${prefix}${$wht(string)} ` : prefix
282
+ let marked = false
283
+
284
+ // arrays (will remove keys for properties)
285
+ if (Array.isArray(value)) {
286
+ if (value.length === 0) {
287
+ line = `${line}${_squares}`
288
+ } else {
289
+ log.warn(`${line}${_opnSqr}`)
290
+ log.enter()
291
+ try {
292
+ for (let i = 0; i < value.length; i ++) {
293
+ const { prefix, suffix } = fixups('', false, true, undefined, color)
294
+ dump(log, value[i], prefix, suffix, color, [ ...stack, value ])
295
+ keys.delete(String(i))
296
+ }
297
+ } finally {
298
+ log.leave()
299
+ }
300
+ line = _clsSqr
301
+ }
302
+ marked = true
303
+
304
+ // arrays, sets and maps are mutually exclusive...
305
+ } else if (value instanceof Set) {
306
+ if (value.size === 0) {
307
+ line = `${line}${_squares}`
308
+ } else {
309
+ log.warn(`${line}${_opnSqr}`)
310
+ log.enter()
311
+ try {
312
+ const { prefix, suffix } = fixups('', false, true, undefined, color)
313
+ value.forEach((v) => dump(log, v, prefix, suffix, color, [ ...stack, value ]))
314
+ } finally {
315
+ log.leave()
316
+ }
317
+ line = _clsSqr
318
+ }
319
+ marked = true
320
+
321
+ // arrays, sets and maps are mutually exclusive...
322
+ } else if (value instanceof Map) {
323
+ if (value.size === 0) {
324
+ line = `${line}${_curls}`
325
+ } else {
326
+ log.warn(`${line}${_opnCrl}`)
327
+ log.enter()
328
+ try {
329
+ for (const [ key, subvalue ] of value) {
330
+ const { prefix, suffix } = fixups(stringifyValue(key), true, true, undefined, color)
331
+ dump(log, subvalue, prefix, suffix, color, [ ...stack, value ])
332
+ }
333
+ } finally {
334
+ log.leave()
335
+ }
336
+ line = _clsCrl
337
+ }
338
+ marked = true
339
+ }
340
+
341
+ // boxed strings leave props around
342
+ if (value instanceof String) {
343
+ const length = value.valueOf().length
344
+ for (let i = 0; i < length; i ++) keys.delete(String(i))
345
+ }
346
+
347
+ // extra properties might appear at any time...
348
+ if (keys.size) {
349
+ if (marked) line = `${line} ${_extraProps} `
350
+ log.warn(`${line}${_opnCrl}`)
351
+ log.enter()
352
+ try {
353
+ for (const key of keys) {
354
+ const { prefix, suffix } = fixups(stringifyValue(key), false, true, undefined, color)
355
+ dump(log, value[key], prefix, suffix, color, [ ...stack, value ])
356
+ }
357
+ } finally {
358
+ log.leave()
359
+ }
360
+ line = _clsCrl
361
+ marked = true
362
+ }
363
+
364
+ if (marked) {
365
+ return `${line}${suffix}`
366
+ } else {
367
+ return `${line}${_curls}${suffix}`
368
+ }
369
+ }
370
+
371
+ /* ========================================================================== *
372
+ * EXPORTSD *
373
+ * ========================================================================== */
374
+
375
+ /** Print a {@link Diff} to a log, with a nice header by default... */
376
+ export function printDiff(log: Logger, diff: Diff, header = true): void {
377
+ if (! header) return printBaseDiff(log, diff, '', false, false)
378
+
379
+ log.warn(_diffHeader)
380
+ log.enter()
381
+ try {
382
+ printBaseDiff(log, diff, '', false, false)
383
+ } finally {
384
+ log.leave()
385
+ }
386
+ }
@@ -0,0 +1,45 @@
1
+ import { ExpectationError, assertType } from './types'
2
+
3
+ import type { Expectation, Expectations } from './expect'
4
+ import type { Constructor, StringMatcher } from './types'
5
+
6
+ export class ToThrow implements Expectation {
7
+ expect(
8
+ context: Expectations,
9
+ negative: boolean,
10
+ assert?: (errorExpectations: Expectations) => void,
11
+ ): void {
12
+ assertType(context, 'function')
13
+
14
+ let thrown: boolean
15
+ let error: unknown
16
+ try {
17
+ context.value()
18
+ thrown = false
19
+ error = undefined
20
+ } catch (caught) {
21
+ thrown = true
22
+ error = caught
23
+ }
24
+
25
+ if (thrown === negative) {
26
+ throw new ExpectationError(context, negative, 'to throw')
27
+ } else if (thrown && assert) {
28
+ assert(context.forValue(error))
29
+ }
30
+ }
31
+ }
32
+
33
+ export class ToThrowError implements Expectation {
34
+ expect(
35
+ context: Expectations,
36
+ negative: boolean,
37
+ ...args:
38
+ | []
39
+ | [ message: StringMatcher ]
40
+ | [ constructor: Constructor<Error> ]
41
+ | [ constructor: Constructor<Error>, message: StringMatcher ]
42
+ ): void {
43
+ context.negated(negative).toThrow((assert) => assert.toBeError(...args))
44
+ }
45
+ }
@@ -0,0 +1,263 @@
1
+ import type { Diff } from './diff'
2
+ import type { Expectations, ExpectationsMatcher } from './expect'
3
+
4
+ /** A type identifying any constructor */
5
+ export type Constructor<T = any> = new (...args: any[]) => T
6
+
7
+ /** A type identifying any function */
8
+ export type Callable<T = any> = (...args: readonly any[]) => T
9
+
10
+ /** A simple _record_ indicating an `object` but never an `array` */
11
+ export type NonArrayObject<T = any> = {
12
+ [a: string]: T
13
+ [b: symbol]: T
14
+ [c: number]: never
15
+ }
16
+
17
+ /** A type identifying the parameter of `string.match(...)` */
18
+ export type StringMatcher = string | RegExp | {
19
+ [Symbol.match](string: string): RegExpMatchArray | null
20
+ }
21
+
22
+ /** Mappings for our _expanded_ {@link typeOf} implementation */
23
+ export type TypeMappings = {
24
+ // standard types, from "typeof"
25
+ bigint: bigint,
26
+ boolean: boolean,
27
+ function: Callable,
28
+ number: number,
29
+ string: string,
30
+ symbol: symbol,
31
+ undefined: undefined,
32
+ // specialized object types
33
+ array: readonly any [],
34
+ buffer: Buffer,
35
+ map: Map<any, any>,
36
+ promise: PromiseLike<any>,
37
+ regexp: RegExp,
38
+ set: Set<any>,
39
+ // object: anything not mapped above, not null, and not an array
40
+ object: NonArrayObject<any>,
41
+ // oh, javascript :-(
42
+ null: null,
43
+ }
44
+
45
+ /** Values returned by our own _expanded_ `{@link typeOf}` */
46
+ export type TypeName = keyof TypeMappings
47
+
48
+ /* ========================================================================== *
49
+ * TYPE INSPECTION, GUARD, AND ASSERTION *
50
+ * ========================================================================== */
51
+
52
+ /** Expanded `typeof` implementation returning some extra types */
53
+ export function typeOf(value: unknown): TypeName {
54
+ if (value === null) return 'null' // oh, javascript :-(
55
+
56
+ // primitive types
57
+ const type = typeof value
58
+ switch (type) {
59
+ case 'bigint':
60
+ case 'boolean':
61
+ case 'function':
62
+ case 'number':
63
+ case 'string':
64
+ case 'symbol':
65
+ case 'undefined':
66
+ return type
67
+ }
68
+
69
+ // extended types
70
+ if (Array.isArray(value)) return 'array'
71
+
72
+ if (value instanceof Promise) return 'promise'
73
+ if (typeof (value as any)['then'] === 'function') return 'promise'
74
+
75
+ if (value instanceof Buffer) return 'buffer'
76
+ if (value instanceof RegExp) return 'regexp'
77
+ if (value instanceof Map) return 'map'
78
+ if (value instanceof Set) return 'set'
79
+
80
+ // anything else is an object
81
+ return 'object'
82
+ }
83
+
84
+ /** Determines if the specified `value` is of the specified _expanded_ `type` */
85
+ export function isType<T extends keyof TypeMappings>(
86
+ context: Expectations,
87
+ type: T,
88
+ ): context is Expectations<TypeMappings[T]> {
89
+ return typeOf(context.value) === type
90
+ }
91
+
92
+ /** Asserts that the specified `value` is of the specified _expanded_ `type` */
93
+ export function assertType<T extends keyof TypeMappings>(
94
+ context: Expectations,
95
+ type: T,
96
+ ): asserts context is Expectations<TypeMappings[T]> {
97
+ const { value } = context
98
+
99
+ if (typeOf(value) === type) return
100
+
101
+ throw new ExpectationError(context, false, `to be ${prefixType(type)}`)
102
+ }
103
+
104
+ /* ========================================================================== *
105
+ * PRETTY PRINTING FOR MESSAGES AND DIFFS *
106
+ * ========================================================================== */
107
+
108
+ /** Get constructor name or default for {@link stringifyValue} */
109
+ function constructorName(value: Record<any, any>): string {
110
+ return Object.getPrototypeOf(value)?.constructor?.name
111
+ }
112
+
113
+ /** Format binary data for {@link stringifyValue} */
114
+ function formatBinaryData(value: Record<any, any>, buffer: Buffer): string {
115
+ const binary = buffer.length > 20 ?
116
+ `${buffer.toString('hex', 0, 20)}\u2026, length=${value.length}` :
117
+ buffer.toString('hex')
118
+ return binary ?
119
+ `[${constructorName(value)}: ${binary}]` :
120
+ `[${constructorName(value)}: empty]`
121
+ }
122
+
123
+ /* ========================================================================== */
124
+
125
+ /** Stringify the type of an object (its constructor name) */
126
+ export function stringifyObjectType(value: object): string {
127
+ const proto = Object.getPrototypeOf(value)
128
+ if (! proto) return '[Object: null prototype]'
129
+ return stringifyConstructor(proto.constructor)
130
+ }
131
+
132
+ /** Stringify a constructor */
133
+ export function stringifyConstructor(ctor: Constructor): string {
134
+ if (! ctor) return '[Object: no constructor]'
135
+ if (! ctor.name) return '[Object: anonymous]'
136
+ return `[${ctor.name}]`
137
+ }
138
+
139
+ /** Pretty print the value (strings, numbers, booleans) or return the type */
140
+ export function stringifyValue(value: unknown): string {
141
+ if (value === null) return '<null>'
142
+ if (value === undefined) return '<undefined>'
143
+
144
+ switch (typeof value) {
145
+ case 'string':
146
+ if (value.length > 40) value = `${value.substring(0, 40)}\u2026, length=${value.length}`
147
+ return JSON.stringify(value)
148
+ case 'number':
149
+ if (value === Number.POSITIVE_INFINITY) return '+Infinity'
150
+ if (value === Number.NEGATIVE_INFINITY) return '-Infinity'
151
+ return String(value)
152
+ case 'boolean':
153
+ return String(value)
154
+ case 'bigint':
155
+ return `${value}n`
156
+ case 'function':
157
+ return value.name ? `<function ${value.name}>` : '<function>'
158
+ case 'symbol':
159
+ return value.description ? `<symbol ${value.description}>`: '<symbol>'
160
+ }
161
+
162
+ // specific object types
163
+ if (isMatcher(value)) return '<matcher>'
164
+ if (value instanceof RegExp) return String(value)
165
+ if (value instanceof Date) return `[${constructorName(value)}: ${value.toISOString()}]`
166
+ if (value instanceof Boolean) return `[${constructorName(value)}: ${value.valueOf()}]`
167
+ if (value instanceof Number) return `[${constructorName(value)}: ${stringifyValue(value.valueOf())}]`
168
+ if (value instanceof String) return `[${constructorName(value)}: ${stringifyValue(value.valueOf())}]`
169
+
170
+ if (Array.isArray(value)) return `[${constructorName(value)} (${value.length})]`
171
+ if (value instanceof Set) return `[${constructorName(value)} (${value.size})]`
172
+ if (value instanceof Map) return `[${constructorName(value)} (${value.size})]`
173
+
174
+ if (value instanceof Buffer) return formatBinaryData(value, value)
175
+ if (value instanceof Uint8Array) return formatBinaryData(value, Buffer.from(value))
176
+ if (value instanceof ArrayBuffer) return formatBinaryData(value, Buffer.from(value))
177
+ if (value instanceof SharedArrayBuffer) return formatBinaryData(value, Buffer.from(value))
178
+
179
+ // inspect anything else...
180
+ return stringifyObjectType(value)
181
+ }
182
+
183
+ /** Add the `a`/`an`/... prefix to the type name */
184
+ export function prefixType(type: TypeName): string {
185
+ switch (type) {
186
+ case 'bigint':
187
+ case 'boolean':
188
+ case 'buffer':
189
+ case 'function':
190
+ case 'map':
191
+ case 'number':
192
+ case 'promise':
193
+ case 'regexp':
194
+ case 'set':
195
+ case 'string':
196
+ case 'symbol':
197
+ return `a <${type}>`
198
+
199
+ case 'array':
200
+ case 'object':
201
+ return `an <${type}>`
202
+
203
+ case 'null':
204
+ case 'undefined':
205
+ return `<${type}>`
206
+
207
+ default:
208
+ return `of unknown type <${type}>`
209
+ }
210
+ }
211
+
212
+ /* ========================================================================== *
213
+ * EXPECTATIONS MATCHERS MARKER *
214
+ * ========================================================================== */
215
+
216
+ export const matcherMarker = Symbol.for('expect5.matcher')
217
+
218
+ export function isMatcher(what: any): what is ExpectationsMatcher {
219
+ return what && what[matcherMarker] === matcherMarker
220
+ }
221
+
222
+ /* ========================================================================== *
223
+ * EXPECTATION ERRORS *
224
+ * ========================================================================== */
225
+
226
+ export class ExpectationError extends Error {
227
+ diff?: Diff | undefined
228
+
229
+ constructor(
230
+ context: Expectations,
231
+ negative: boolean,
232
+ details: string,
233
+ diff?: Diff,
234
+ ) {
235
+ const { value } = context
236
+ const not = negative ? ' not' : ''
237
+
238
+ // if we're not root...
239
+ let preamble = stringifyValue(value)
240
+ if (context.parent) {
241
+ const properties: any[] = []
242
+
243
+ while (context.parent) {
244
+ properties.push(`[${stringifyValue(context.parent.prop)}]`)
245
+ context = context.parent.context
246
+ }
247
+
248
+ preamble = properties.reverse().join('')
249
+
250
+ // type of root value is constructor without details
251
+ const type = typeof context.value === 'object' ?
252
+ stringifyObjectType(context.value as object) : // parent values can not be null!
253
+ stringifyValue(context.value)
254
+
255
+ // assemble the preamble
256
+ preamble = `property ${preamble} of ${type} (${stringifyValue(value)})`
257
+ }
258
+
259
+ super(`Expected ${preamble}${not} ${details}`)
260
+
261
+ if (diff) this.diff = diff
262
+ }
263
+ }