@yamf/test 0.1.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 +428 -0
- package/package.json +39 -0
- package/src/assert.js +504 -0
- package/src/assertion-errors.js +114 -0
- package/src/cli.js +49 -0
- package/src/helpers.js +69 -0
- package/src/index.js +49 -0
- package/src/runner.js +329 -0
- package/src/tap-reporter.js +2 -0
package/src/assert.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssertionFailure,
|
|
3
|
+
AssertionFailureDetail,
|
|
4
|
+
MultiAssertionFailure,
|
|
5
|
+
} from './assertion-errors.js'
|
|
6
|
+
|
|
7
|
+
// TODO @yamf/core
|
|
8
|
+
import { Logger } from '../../core/src/index.js'
|
|
9
|
+
const logger = new Logger()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function isAsyncOrPromise(arg) {
|
|
13
|
+
return arg instanceof Promise || arg?.constructor?.name === 'AsyncFunction'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// used to check what mode to run in
|
|
17
|
+
function scanAllArgumentsForAsyncOrPromise(args, assertFns) {
|
|
18
|
+
if (!Array.isArray(args)) {
|
|
19
|
+
if (isAsyncOrPromise(args)) return true
|
|
20
|
+
} else {
|
|
21
|
+
for (let arg of args) if (isAsyncOrPromise(arg)) return true
|
|
22
|
+
for (let fn of assertFns) if (isAsyncOrPromise(fn)) return true
|
|
23
|
+
}
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// validator for sequence fns (expects a matching assertionFn length)
|
|
28
|
+
function validateSequenceAssertionCount(targets, assertFns) {
|
|
29
|
+
if (!Array.isArray(targets) || !Array.isArray(assertFns))
|
|
30
|
+
throw new Error('Expected sequence assertion arguments to be arrays')
|
|
31
|
+
if (targets.length !== assertFns.length)
|
|
32
|
+
throw new Error('Expected sequence assertion arguments to be the same length')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// for multi-assertion meta-helpers (each, sequence)
|
|
36
|
+
function checkFailuresAndThrowMultiAssertionFailure(assertionName, failures) {
|
|
37
|
+
if (failures.length === 0) return
|
|
38
|
+
if (failures.length > 1) {
|
|
39
|
+
throw new MultiAssertionFailure(assertionName, failures)
|
|
40
|
+
} else if (failures.length === 1) {
|
|
41
|
+
throw failures[0]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function distillValueFromTestArgument(val) {
|
|
46
|
+
try {
|
|
47
|
+
if (typeof val === 'function') val = val()
|
|
48
|
+
} catch (err) {
|
|
49
|
+
val = err
|
|
50
|
+
}
|
|
51
|
+
// if we find a promise, throw so we can attempt to run as async
|
|
52
|
+
if (val instanceof Promise) {
|
|
53
|
+
let err = new Error('Found Promise in Sync Fn')
|
|
54
|
+
err.promise = val
|
|
55
|
+
throw err
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return val
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function distillValueFromTestArgumentAsync(val) {
|
|
62
|
+
try {
|
|
63
|
+
if (typeof val === 'function') {
|
|
64
|
+
val = await val()
|
|
65
|
+
} else if (val instanceof Promise) {
|
|
66
|
+
return val.then(a => val = a).catch(e => val = e)
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
val = err
|
|
70
|
+
}
|
|
71
|
+
return val
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
function getAssertionFailuresForValue(val, ...assertFns) {
|
|
76
|
+
let assertionFailures = []
|
|
77
|
+
if (!(val instanceof Error)) {
|
|
78
|
+
let failedAssertFns = []
|
|
79
|
+
assertFns.forEach(assertFn => {
|
|
80
|
+
try {
|
|
81
|
+
let isValid
|
|
82
|
+
if (typeof assertFn === 'function') {
|
|
83
|
+
isValid = assertFn(val)
|
|
84
|
+
} else {
|
|
85
|
+
isValid = val === assertFn
|
|
86
|
+
}
|
|
87
|
+
if (!isValid) {
|
|
88
|
+
failedAssertFns.push(assertFn)
|
|
89
|
+
}
|
|
90
|
+
} catch (assertionErr) {
|
|
91
|
+
failedAssertFns.push(assertionErr)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
if (failedAssertFns.length > 0) {
|
|
95
|
+
assertionFailures.push(new AssertionFailureDetail(val, failedAssertFns))
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
assertionFailures.push(new AssertionFailureDetail(val, 'expected no error'))
|
|
99
|
+
}
|
|
100
|
+
return assertionFailures
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function getAssertionFailuresForValueAsync(val, ...assertFns) {
|
|
104
|
+
let assertionFailures = []
|
|
105
|
+
if (!(val instanceof Error)) {
|
|
106
|
+
let failedAssertFns = []
|
|
107
|
+
await Promise.all(assertFns.map(async assertFn => {
|
|
108
|
+
try {
|
|
109
|
+
let isValid
|
|
110
|
+
if (typeof assertFn === 'function') {
|
|
111
|
+
isValid = await assertFn(val)
|
|
112
|
+
} else {
|
|
113
|
+
isValid = val === assertFn
|
|
114
|
+
}
|
|
115
|
+
if (!isValid) {
|
|
116
|
+
failedAssertFns.push(assertFn)
|
|
117
|
+
}
|
|
118
|
+
} catch (assertionErr) {
|
|
119
|
+
failedAssertFns.push(assertionErr)
|
|
120
|
+
}
|
|
121
|
+
}))
|
|
122
|
+
if (failedAssertFns.length > 0) {
|
|
123
|
+
assertionFailures.push(new AssertionFailureDetail(val, failedAssertFns))
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
assertionFailures.push(new AssertionFailureDetail(val, 'expected no error'))
|
|
127
|
+
}
|
|
128
|
+
return assertionFailures
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getErrorAssertionFailuresForValue(val, ...assertFns) {
|
|
132
|
+
let assertionFailures = []
|
|
133
|
+
if (val instanceof Error) {
|
|
134
|
+
let failedAssertFns = []
|
|
135
|
+
assertFns.forEach(assertFn => {
|
|
136
|
+
try {
|
|
137
|
+
let isValid = assertFn(val)
|
|
138
|
+
if (!isValid) {
|
|
139
|
+
failedAssertFns.push(assertFn)
|
|
140
|
+
}
|
|
141
|
+
} catch (assertionErr) {
|
|
142
|
+
failedAssertFns.push(assertionErr)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
if (failedAssertFns.length > 0) {
|
|
146
|
+
assertionFailures.push(new AssertionFailureDetail(val, failedAssertFns))
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
assertionFailures.push(new AssertionFailureDetail(val, 'expected an error'))
|
|
150
|
+
}
|
|
151
|
+
return assertionFailures
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function getErrorAssertionFailuresForValueAsync(val, ...assertFns) {
|
|
155
|
+
let assertionFailures = []
|
|
156
|
+
if (val instanceof Error) {
|
|
157
|
+
let failedAssertFns = []
|
|
158
|
+
await Promise.all(assertFns.map(async assertFn => {
|
|
159
|
+
try {
|
|
160
|
+
let isValid = await assertFn(val)
|
|
161
|
+
if (!isValid) {
|
|
162
|
+
failedAssertFns.push(assertFn)
|
|
163
|
+
}
|
|
164
|
+
} catch (assertionErr) {
|
|
165
|
+
failedAssertFns.push(assertionErr)
|
|
166
|
+
}
|
|
167
|
+
}))
|
|
168
|
+
if (failedAssertFns.length > 0) {
|
|
169
|
+
assertionFailures.push(new AssertionFailureDetail(val, failedAssertFns))
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
assertionFailures.push(new AssertionFailureDetail(val, 'expected an error'))
|
|
173
|
+
}
|
|
174
|
+
return assertionFailures
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getTransformedAssertionFailuresforValue(val, ...assertFns) {
|
|
178
|
+
let assertionFailures = getAssertionFailuresForValue(val, ...assertFns)
|
|
179
|
+
let failure = checkAndTransformErrorsToMultiAssertError(val, assertionFailures)
|
|
180
|
+
return failure
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function getTransformedAssertionFailuresforValueAsync(val, ...assertFns) {
|
|
184
|
+
let assertionFailures = await getAssertionFailuresForValueAsync(val, ...assertFns)
|
|
185
|
+
let failure = checkAndTransformErrorsToMultiAssertError(val, assertionFailures)
|
|
186
|
+
return failure
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getTransformedErrorAssertionFailuresforValue(val, ...assertFns) {
|
|
190
|
+
let assertionFailures = getErrorAssertionFailuresForValue(val, ...assertFns)
|
|
191
|
+
let failure = checkAndTransformErrorsToMultiAssertError(val, assertionFailures)
|
|
192
|
+
return failure
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function getTransformedErrorAssertionFailuresforValueAsync(val, ...assertFns) {
|
|
196
|
+
let assertionFailures = await getErrorAssertionFailuresForValueAsync(val, ...assertFns)
|
|
197
|
+
let failure = checkAndTransformErrorsToMultiAssertError(val, assertionFailures)
|
|
198
|
+
return failure
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function checkAndTransformErrorsToMultiAssertError(val, assertionFailures) {
|
|
202
|
+
if (Array.isArray(assertionFailures) && assertionFailures.length > 0) {
|
|
203
|
+
let failure
|
|
204
|
+
if (assertionFailures.length === 1) {
|
|
205
|
+
failure = new AssertionFailure(val, assertionFailures[0])
|
|
206
|
+
} else {
|
|
207
|
+
failure = new AssertionFailure(val, assertionFailures)
|
|
208
|
+
}
|
|
209
|
+
return failure
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// main assertion helper entry-point
|
|
214
|
+
function processAssertionAndChooseSyncOrAsyncPath(assertionHelper, target, ...assertFns) {
|
|
215
|
+
const doAsyncPath = (thrownPromise) => new Promise((resolve, reject) => assertionHelper
|
|
216
|
+
// use the val from the (sync) thrown promise target
|
|
217
|
+
.async(thrownPromise || target, ...assertFns)
|
|
218
|
+
.catch(reject)
|
|
219
|
+
.finally(resolve)
|
|
220
|
+
)
|
|
221
|
+
if (scanAllArgumentsForAsyncOrPromise(target, assertFns)) {
|
|
222
|
+
return doAsyncPath()
|
|
223
|
+
} else try {
|
|
224
|
+
return assertionHelper.sync(target, ...assertFns)
|
|
225
|
+
} catch (err) {
|
|
226
|
+
if (err.message?.includes('Found Promise')) {
|
|
227
|
+
return doAsyncPath(err.promise)
|
|
228
|
+
} else throw err
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Assert that a value or function result passes all assertion functions
|
|
234
|
+
*
|
|
235
|
+
* Automatically detects and handles async when needed:
|
|
236
|
+
* - Direct Promise: `await assert(promise, fn)`
|
|
237
|
+
* - Async function: `await assert(async () => await fetchData(), fn)`
|
|
238
|
+
* - Promise-returning function: `await assert(() => fetchData(), fn)`
|
|
239
|
+
* - Sync function: `assert(() => getValue(), fn)`
|
|
240
|
+
* - Direct value: `assert(5, fn)`
|
|
241
|
+
*
|
|
242
|
+
* @param {*|Function|Promise} valOrFn - Value, function, or Promise to test
|
|
243
|
+
* @param {...Function} assertFns - Assertion functions returning true/false
|
|
244
|
+
* @returns {void|Promise<void>} - Throws AssertError or MultiAssertError on failure
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* // Sync usage
|
|
248
|
+
* assert(5, n => n > 0)
|
|
249
|
+
* assert(() => getValue(), n => n > 0)
|
|
250
|
+
*
|
|
251
|
+
* // Async usage (auto-detected, just add await)
|
|
252
|
+
* await assert(promise, v => v !== null)
|
|
253
|
+
* await assert(async () => await fetchData(), d => d.status === 'ok')
|
|
254
|
+
* await assert(() => fetchData(), d => d.status === 'ok') // Promise-returning
|
|
255
|
+
*/
|
|
256
|
+
|
|
257
|
+
export function assert(valOrFn, ...assertFns) {
|
|
258
|
+
return processAssertionAndChooseSyncOrAsyncPath(assert, valOrFn, ...assertFns)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
assert.sync = function assertSync(valOrFn, ...assertFns) {
|
|
262
|
+
let val = distillValueFromTestArgument(valOrFn)
|
|
263
|
+
let failure = getTransformedAssertionFailuresforValue(val, ...assertFns)
|
|
264
|
+
if (failure) throw failure
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
assert.async = async function assertAsync(valOrFn, ...assertFns) {
|
|
268
|
+
let val = await distillValueFromTestArgumentAsync(valOrFn)
|
|
269
|
+
let failure = await getTransformedAssertionFailuresforValueAsync(val, ...assertFns)
|
|
270
|
+
if (failure) throw failure
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Assert that a function throws an error matching all assertion functions
|
|
275
|
+
*
|
|
276
|
+
* Automatically detects and handles async when needed:
|
|
277
|
+
* - Direct Error: `assertErr(error, fn)`
|
|
278
|
+
* - Sync throwing function: `assertErr(() => throwingFn(), fn)`
|
|
279
|
+
* - Async throwing function: `await assertErr(async () => await failingFn(), fn)`
|
|
280
|
+
* - Promise-rejecting function: `await assertErr(() => rejectingFn(), fn)`
|
|
281
|
+
*
|
|
282
|
+
* @param {Error|Function} errOrFn - Error object or function that should throw
|
|
283
|
+
* @param {...Function} assertFns - Assertion functions returning true/false
|
|
284
|
+
* @returns {void|Promise<void>} - Throws AssertError or MultiAssertError on failure
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* // Sync usage
|
|
288
|
+
* assertErr(() => throwingFn(), err => err.message.includes('expected'))
|
|
289
|
+
* assertErr(new Error('test'), err => err.message === 'test')
|
|
290
|
+
*
|
|
291
|
+
* // Async usage (auto-detected, just add await)
|
|
292
|
+
* await assertErr(async () => await failingAsyncFn(), err => err.status === 400)
|
|
293
|
+
* await assertErr(() => rejectingPromiseFn(), err => err.message === 'rejected')
|
|
294
|
+
*/
|
|
295
|
+
export function assertErr(errOrFn, ...assertFns) {
|
|
296
|
+
return processAssertionAndChooseSyncOrAsyncPath(assertErr, errOrFn, ...assertFns)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
assertErr.sync = function assertErrSync(errOrFn, ...assertFns) {
|
|
300
|
+
let val = distillValueFromTestArgument(errOrFn)
|
|
301
|
+
let failure = getTransformedErrorAssertionFailuresforValue(val, ...assertFns)
|
|
302
|
+
if (failure) throw failure
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
assertErr.async = async function assertErrAsync(errOrFn, ...assertFns) {
|
|
306
|
+
let val = await distillValueFromTestArgumentAsync(errOrFn)
|
|
307
|
+
let failure = await getTransformedErrorAssertionFailuresforValueAsync(val, ...assertFns)
|
|
308
|
+
if (failure) throw failure
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Assert that each value in an array passes all assertion functions
|
|
314
|
+
*
|
|
315
|
+
* Automatically detects and handles async when needed.
|
|
316
|
+
* Applies each assertion function to each value in the array.
|
|
317
|
+
*
|
|
318
|
+
* @param {Array} values - Array of values to test
|
|
319
|
+
* @param {...Function} assertFns - Assertion functions returning true/false
|
|
320
|
+
* @returns {void|Promise<void>} - Throws AssertError or MultiAssertError on failure
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* // Sync usage
|
|
324
|
+
* assertEach([1, 2, 3],
|
|
325
|
+
* n => n > 0,
|
|
326
|
+
* n => n < 10
|
|
327
|
+
* )
|
|
328
|
+
*
|
|
329
|
+
* // Async usage (auto-detected, just add await)
|
|
330
|
+
* await assertEach([promise1, promise2],
|
|
331
|
+
* val => val !== null,
|
|
332
|
+
* val => val.status === 'ok'
|
|
333
|
+
* )
|
|
334
|
+
*/
|
|
335
|
+
export function assertEach(values, ...assertFns) {
|
|
336
|
+
return processAssertionAndChooseSyncOrAsyncPath(assertEach, values, ...assertFns)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
assertEach.sync = function assertEachSync(values, ...assertFns) {
|
|
340
|
+
let failures = []
|
|
341
|
+
for (let val of values) {
|
|
342
|
+
val = distillValueFromTestArgument(val)
|
|
343
|
+
let failure = getTransformedAssertionFailuresforValue(val, ...assertFns)
|
|
344
|
+
if (failure) failures.push(failure)
|
|
345
|
+
}
|
|
346
|
+
checkFailuresAndThrowMultiAssertionFailure('assertEachSync', failures)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
assertEach.async = async function assertEachAsync(values, ...assertFns) {
|
|
350
|
+
let failures = []
|
|
351
|
+
for (let val of values) {
|
|
352
|
+
val = await distillValueFromTestArgumentAsync(val)
|
|
353
|
+
let failure = await getTransformedAssertionFailuresforValueAsync(val, ...assertFns)
|
|
354
|
+
if (failure) failures.push(failure)
|
|
355
|
+
}
|
|
356
|
+
checkFailuresAndThrowMultiAssertionFailure('assertEachAsync', failures)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Assert that each value in an array passes the corresponding assertion function
|
|
361
|
+
*
|
|
362
|
+
* Automatically detects and handles async when needed.
|
|
363
|
+
* Applies the corresponding assertion function to each value in the array.
|
|
364
|
+
* Errors early if the number of values and assertion functions do not match.
|
|
365
|
+
*
|
|
366
|
+
* @param {Array} values - Array of values to test
|
|
367
|
+
* @param {...Function} assertFns - Assertion functions returning true/false
|
|
368
|
+
* @returns {void|Promise<void>} - Throws AssertError or MultiAssertError on failure
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* // Sync usage
|
|
372
|
+
* assertSequence([1, 2, 3],
|
|
373
|
+
* n => n === 1,
|
|
374
|
+
* n => n === 2,
|
|
375
|
+
* n => n === 3
|
|
376
|
+
* )
|
|
377
|
+
*
|
|
378
|
+
* // Async usage (auto-detected, just add await)
|
|
379
|
+
* await assertSequence([promise1, promise2, promise3],
|
|
380
|
+
* val => val !== null,
|
|
381
|
+
* val => val.status === 'ok',
|
|
382
|
+
* val => val.count > 0
|
|
383
|
+
* )
|
|
384
|
+
*/
|
|
385
|
+
export function assertSequence(values, ...assertFns) {
|
|
386
|
+
validateSequenceAssertionCount(values, assertFns)
|
|
387
|
+
return processAssertionAndChooseSyncOrAsyncPath(assertSequence, values, ...assertFns)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
assertSequence.sync = function assertSequenceSync(values, ...assertFns) {
|
|
391
|
+
let failures = []
|
|
392
|
+
let index = 0
|
|
393
|
+
for (let val of values) {
|
|
394
|
+
val = distillValueFromTestArgument(val)
|
|
395
|
+
let failure = getTransformedAssertionFailuresforValue(val, assertFns[index])
|
|
396
|
+
if (failure) failures.push(failure)
|
|
397
|
+
index++
|
|
398
|
+
}
|
|
399
|
+
checkFailuresAndThrowMultiAssertionFailure('assertSequenceSync', failures)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
assertSequence.async = async function assertSequenceAsync(values, ...assertFns) {
|
|
403
|
+
let failures = []
|
|
404
|
+
let index = 0
|
|
405
|
+
for (let val of values) {
|
|
406
|
+
val = await distillValueFromTestArgumentAsync(val)
|
|
407
|
+
let failure = await getTransformedAssertionFailuresforValueAsync(val, assertFns[index])
|
|
408
|
+
if (failure) failures.push(failure)
|
|
409
|
+
index++
|
|
410
|
+
}
|
|
411
|
+
checkFailuresAndThrowMultiAssertionFailure('assertSequenceAsync', failures)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Assert that each error in an array passes all assertion functions
|
|
417
|
+
* Assert that each function in an array throws an error matching all assertion functions
|
|
418
|
+
*
|
|
419
|
+
* Automatically detects and handles async when needed.
|
|
420
|
+
*
|
|
421
|
+
* @param {Array} errsOrFns - Array of errors or functions that should throw
|
|
422
|
+
* @param {...Function} assertFns - Assertion functions returning true/false
|
|
423
|
+
* @returns {void|Promise<void>} - Throws AssertError or MultiAssertError on failure
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* // Sync usage with error objects
|
|
427
|
+
* assertErrEach([new Error('test'), new Error('test2')],
|
|
428
|
+
* err => err instanceof Error,
|
|
429
|
+
* err => err.message.includes('test')
|
|
430
|
+
* )
|
|
431
|
+
*
|
|
432
|
+
* // Sync usage with throwing functions
|
|
433
|
+
* assertErrEach([
|
|
434
|
+
* () => { throw new Error('error1') },
|
|
435
|
+
* () => { throw new Error('error2') }
|
|
436
|
+
* ],
|
|
437
|
+
* err => err instanceof Error,
|
|
438
|
+
* err => err.message.includes('error')
|
|
439
|
+
* )
|
|
440
|
+
*
|
|
441
|
+
* // Async usage (auto-detected, just add await)
|
|
442
|
+
* await assertErrEach([
|
|
443
|
+
* async () => { throw new Error('async1') },
|
|
444
|
+
* async () => { throw new Error('async2') }
|
|
445
|
+
* ],
|
|
446
|
+
* err => err instanceof Error,
|
|
447
|
+
* err => err.message.includes('async')
|
|
448
|
+
* )
|
|
449
|
+
*/
|
|
450
|
+
export function assertErrEach(errsOrFns, ...assertFns) {
|
|
451
|
+
return processAssertionAndChooseSyncOrAsyncPath(assertErrEach, errsOrFns, ...assertFns)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
assertErrEach.sync = function assertErrEachSync(errsOrFns, ...assertFns) {
|
|
455
|
+
let failures = []
|
|
456
|
+
for (let errOrFn of errsOrFns) {
|
|
457
|
+
let val = distillValueFromTestArgument(errOrFn)
|
|
458
|
+
let failure = getTransformedErrorAssertionFailuresforValue(val, ...assertFns)
|
|
459
|
+
if (failure) failures.push(failure)
|
|
460
|
+
}
|
|
461
|
+
checkFailuresAndThrowMultiAssertionFailure('assertErrEachSync', failures)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
assertErrEach.async = async function assertErrEachAsync(errsOrFns, ...assertFns) {
|
|
465
|
+
let failures = []
|
|
466
|
+
for (let errOrFn of errsOrFns) {
|
|
467
|
+
let val = await distillValueFromTestArgumentAsync(errOrFn)
|
|
468
|
+
let failure = await getTransformedErrorAssertionFailuresforValueAsync(val, ...assertFns)
|
|
469
|
+
if (failure) failures.push(failure)
|
|
470
|
+
}
|
|
471
|
+
checkFailuresAndThrowMultiAssertionFailure('assertErrEachAsync', failures)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
/* TODO documentation comment
|
|
476
|
+
*/
|
|
477
|
+
export function assertErrSequence(errsOrFns, ...assertFns) {
|
|
478
|
+
validateSequenceAssertionCount(errsOrFns, assertFns)
|
|
479
|
+
return processAssertionAndChooseSyncOrAsyncPath(assertErrSequence, errsOrFns, ...assertFns)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
assertErrSequence.sync = function assertErrSequenceSync(errsOrFns, ...assertFns) {
|
|
483
|
+
let failures = []
|
|
484
|
+
let index = 0
|
|
485
|
+
for (let errOrFn of errsOrFns) {
|
|
486
|
+
let val = distillValueFromTestArgument(errOrFn)
|
|
487
|
+
let failure = getTransformedErrorAssertionFailuresforValue(val, assertFns[index])
|
|
488
|
+
if (failure) failures.push(failure)
|
|
489
|
+
index++
|
|
490
|
+
}
|
|
491
|
+
checkFailuresAndThrowMultiAssertionFailure('assertErrSequenceSync', failures)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
assertErrSequence.async = async function assertErrSequenceAsync(errsOrFns, ...assertFns) {
|
|
495
|
+
let failures = []
|
|
496
|
+
let index = 0
|
|
497
|
+
for (let errOrFn of errsOrFns) {
|
|
498
|
+
let val = await distillValueFromTestArgumentAsync(errOrFn)
|
|
499
|
+
let failure = await getTransformedErrorAssertionFailuresforValueAsync(val, assertFns[index])
|
|
500
|
+
if (failure) failures.push(failure)
|
|
501
|
+
index++
|
|
502
|
+
}
|
|
503
|
+
checkFailuresAndThrowMultiAssertionFailure('assertErrSequenceAsync', failures)
|
|
504
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
|
|
2
|
+
const getType = (val) => {
|
|
3
|
+
if (val instanceof Error) {
|
|
4
|
+
return `error`
|
|
5
|
+
} else return typeof val
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const getTargetString = (val, pad) => {
|
|
9
|
+
if (val instanceof Error) {
|
|
10
|
+
return `"${val.message}"`
|
|
11
|
+
} else if (Array.isArray(val) && val.some(v => v instanceof Error)) {
|
|
12
|
+
return `[${val.map(v => `\n${pad+pad}${getValOrErrString(v)}`).join()}\n${pad}]`
|
|
13
|
+
} else if (typeof val === 'object') {
|
|
14
|
+
return JSON.stringify(val)
|
|
15
|
+
} else {
|
|
16
|
+
return val.toString()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class AssertionFailureDetail {
|
|
21
|
+
constructor(distilledTargetValue, failingAssertionFns, childDetails) {
|
|
22
|
+
this.target = distilledTargetValue
|
|
23
|
+
this.assertFns = (
|
|
24
|
+
Array.isArray(failingAssertionFns)
|
|
25
|
+
&& failingAssertionFns.length > 1
|
|
26
|
+
) ? failingAssertionFns : [failingAssertionFns]
|
|
27
|
+
if (!this.assertFns) this.assertFns = []
|
|
28
|
+
this.children = childDetails
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
toString(depth = 1) {
|
|
32
|
+
let pad = Array.from({ length: depth }, () => ' ').join('')
|
|
33
|
+
let type = getType(this.target)
|
|
34
|
+
let val = getTargetString(this.target, pad)
|
|
35
|
+
let childMessages = this.children?.map(c => {
|
|
36
|
+
`${pad}${c.toString(depth).split('\n').map(l => `${pad}${l}`).join('\n')}`
|
|
37
|
+
})
|
|
38
|
+
childMessages = childMessages || ''
|
|
39
|
+
return `${pad}for target (${type}) value = ${val}`
|
|
40
|
+
+ this.assertFns.map(fn =>
|
|
41
|
+
`\n${pad}failed -> ${fn?.name || fn.toString().replace(/^\s?.+\=\>\s?/, '')}`
|
|
42
|
+
).join('')
|
|
43
|
+
+ childMessages
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get message() {
|
|
47
|
+
return this.toString()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get assertMessage() {
|
|
51
|
+
// 0 for no padding, runner will print this on the same initial line
|
|
52
|
+
return this.toString(0)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class AssertionFailure extends Error {
|
|
57
|
+
constructor(distilledTargetObject, assertionFailureDetails) {
|
|
58
|
+
super() // don't care about original message
|
|
59
|
+
this.target = distilledTargetObject
|
|
60
|
+
this.assertionFailureDetails = (
|
|
61
|
+
Array.isArray(assertionFailureDetails)
|
|
62
|
+
&& assertionFailureDetails.length > 1
|
|
63
|
+
) ? assertionFailureDetails : [assertionFailureDetails]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toString(depth = 1) {
|
|
67
|
+
let pad = Array.from({ length: depth }, () => ' ').join('')
|
|
68
|
+
return `${pad}AssertionFailure\n`
|
|
69
|
+
+ this.assertionFailureDetails?.map(d =>
|
|
70
|
+
`${d.toString(depth+1).split('\n').map(l => `${l}`).join('\n')}`
|
|
71
|
+
).join('\n')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get message() {
|
|
75
|
+
return this.toString()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get assertMessage() {
|
|
79
|
+
// 0 for no padding, runner will print this on the same initial line
|
|
80
|
+
return this.toString(0)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export class MultiAssertionFailure extends Error {
|
|
86
|
+
constructor(assertType, failures) {
|
|
87
|
+
super() // don't care about normal error message
|
|
88
|
+
|
|
89
|
+
if (!failures) {
|
|
90
|
+
failures = assertType
|
|
91
|
+
assertType = ''
|
|
92
|
+
}
|
|
93
|
+
this.assertType = assertType
|
|
94
|
+
this.failures = failures
|
|
95
|
+
this.name = 'MultiAssertionFailure'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
toString() {
|
|
99
|
+
let typeMessage = this.assertType
|
|
100
|
+
? `: "${this.assertType}" failed`
|
|
101
|
+
: ''
|
|
102
|
+
|
|
103
|
+
return `${this.name}${typeMessage}\n`
|
|
104
|
+
+ this.failures.map(f => f.toString(1)).join('\n')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get message() {
|
|
108
|
+
return this.toString()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get assertMessage() {
|
|
112
|
+
return this.toString()
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
/* TODO add cli options and flags
|
|
3
|
+
|
|
4
|
+
--name <name> (matching text or wildcard)
|
|
5
|
+
--file <file> (matching file name text or wildcard; is a test suite by default)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
---TODO determine features and flags from the following suggested list---
|
|
9
|
+
|
|
10
|
+
Essential test CLI options and flags vary significantly depending on the specific testing framework or tool being used. However, common categories of options and flags found across many testing CLIs include:
|
|
11
|
+
Execution Control:
|
|
12
|
+
|
|
13
|
+
Running specific tests:
|
|
14
|
+
--test-name <name> or similar: Executes a single test or a group of tests matching a pattern.
|
|
15
|
+
--grep <pattern>: Filters tests based on a regular expression pattern in their names.
|
|
16
|
+
--file <path>: Runs tests only from specified files.
|
|
17
|
+
Controlling execution flow:
|
|
18
|
+
--fail-fast or --first: Stops test execution immediately upon the first failure.
|
|
19
|
+
--bail: Similar to fail-fast, often with more granular control over how many failures are tolerated before stopping.
|
|
20
|
+
--retries <n>: Reruns failed tests a specified number of times.
|
|
21
|
+
|
|
22
|
+
Output and Reporting:
|
|
23
|
+
|
|
24
|
+
Verbosity levels:
|
|
25
|
+
--verbose or -v: Provides more detailed output during test execution.
|
|
26
|
+
--quiet or -q: Suppresses most output, showing only essential information like summaries.
|
|
27
|
+
Reporting formats:
|
|
28
|
+
--reporter <format>: Specifies the output format for test results (e.g., json, junit, html).
|
|
29
|
+
--output <file>: Redirects test output to a specified file.
|
|
30
|
+
Code coverage:
|
|
31
|
+
--coverage: Enables code coverage analysis and reporting.
|
|
32
|
+
--coverage-report <format>: Specifies the format for coverage reports.
|
|
33
|
+
|
|
34
|
+
Configuration and Setup:
|
|
35
|
+
|
|
36
|
+
Configuration files:
|
|
37
|
+
--config <file>: Specifies an alternative configuration file for the test runner.
|
|
38
|
+
Environment variables:
|
|
39
|
+
--env <key=value>: Sets environment variables for the test process.
|
|
40
|
+
Setup/Teardown scripts:
|
|
41
|
+
--pre-flight <script>: Runs a script or command before any tests execute.
|
|
42
|
+
--post-flight <script>: Runs a script or command after all tests have completed.
|
|
43
|
+
|
|
44
|
+
Debugging and Development:
|
|
45
|
+
|
|
46
|
+
Debugging tools:
|
|
47
|
+
--inspect or --debug: Enables debugging features, often allowing attachment of a debugger.
|
|
48
|
+
--watch: Reruns tests automatically when relevant files change.
|
|
49
|
+
*/
|