@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.
- package/README.md +7 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +96 -0
- package/dist/cli.mjs.map +6 -0
- package/dist/execution/executable.cjs +299 -0
- package/dist/execution/executable.cjs.map +6 -0
- package/dist/execution/executable.d.ts +87 -0
- package/dist/execution/executable.mjs +260 -0
- package/dist/execution/executable.mjs.map +6 -0
- package/dist/execution/executor.cjs +125 -0
- package/dist/execution/executor.cjs.map +6 -0
- package/dist/execution/executor.d.ts +35 -0
- package/dist/execution/executor.mjs +90 -0
- package/dist/execution/executor.mjs.map +6 -0
- package/dist/execution/setup.cjs +127 -0
- package/dist/execution/setup.cjs.map +6 -0
- package/dist/execution/setup.d.ts +31 -0
- package/dist/execution/setup.mjs +87 -0
- package/dist/execution/setup.mjs.map +6 -0
- package/dist/expectation/basic.cjs +216 -0
- package/dist/expectation/basic.cjs.map +6 -0
- package/dist/expectation/basic.d.ts +47 -0
- package/dist/expectation/basic.mjs +177 -0
- package/dist/expectation/basic.mjs.map +6 -0
- package/dist/expectation/diff.cjs +253 -0
- package/dist/expectation/diff.cjs.map +6 -0
- package/dist/expectation/diff.d.ts +27 -0
- package/dist/expectation/diff.mjs +228 -0
- package/dist/expectation/diff.mjs.map +6 -0
- package/dist/expectation/expect.cjs +211 -0
- package/dist/expectation/expect.cjs.map +6 -0
- package/dist/expectation/expect.d.ts +140 -0
- package/dist/expectation/expect.mjs +219 -0
- package/dist/expectation/expect.mjs.map +6 -0
- package/dist/expectation/include.cjs +187 -0
- package/dist/expectation/include.cjs.map +6 -0
- package/dist/expectation/include.d.ts +10 -0
- package/dist/expectation/include.mjs +158 -0
- package/dist/expectation/include.mjs.map +6 -0
- package/dist/expectation/print.cjs +281 -0
- package/dist/expectation/print.cjs.map +6 -0
- package/dist/expectation/print.d.ts +4 -0
- package/dist/expectation/print.mjs +256 -0
- package/dist/expectation/print.mjs.map +6 -0
- package/dist/expectation/throwing.cjs +58 -0
- package/dist/expectation/throwing.cjs.map +6 -0
- package/dist/expectation/throwing.d.ts +8 -0
- package/dist/expectation/throwing.mjs +32 -0
- package/dist/expectation/throwing.mjs.map +6 -0
- package/dist/expectation/types.cjs +212 -0
- package/dist/expectation/types.cjs.map +6 -0
- package/dist/expectation/types.d.ts +57 -0
- package/dist/expectation/types.mjs +178 -0
- package/dist/expectation/types.mjs.map +6 -0
- package/dist/expectation/void.cjs +111 -0
- package/dist/expectation/void.cjs.map +6 -0
- package/dist/expectation/void.d.ts +39 -0
- package/dist/expectation/void.mjs +77 -0
- package/dist/expectation/void.mjs.map +6 -0
- package/dist/globals.cjs +2 -0
- package/dist/globals.cjs.map +6 -0
- package/dist/globals.d.ts +23 -0
- package/dist/globals.mjs +1 -0
- package/dist/globals.mjs.map +6 -0
- package/dist/index.cjs +66 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.mjs +41 -0
- package/dist/index.mjs.map +6 -0
- package/dist/test.cjs +229 -0
- package/dist/test.cjs.map +6 -0
- package/dist/test.d.ts +9 -0
- package/dist/test.mjs +194 -0
- package/dist/test.mjs.map +6 -0
- package/package.json +57 -0
- package/src/cli.mts +122 -0
- package/src/execution/executable.ts +364 -0
- package/src/execution/executor.ts +146 -0
- package/src/execution/setup.ts +108 -0
- package/src/expectation/basic.ts +209 -0
- package/src/expectation/diff.ts +445 -0
- package/src/expectation/expect.ts +401 -0
- package/src/expectation/include.ts +184 -0
- package/src/expectation/print.ts +386 -0
- package/src/expectation/throwing.ts +45 -0
- package/src/expectation/types.ts +263 -0
- package/src/expectation/void.ts +80 -0
- package/src/globals.ts +30 -0
- package/src/index.ts +54 -0
- 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
|
+
}
|