@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,364 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A _callable_ (possibly async) function.
|
|
6
|
+
*
|
|
7
|
+
* When the timeout configured is reached, the passed `signal` will be aborted.
|
|
8
|
+
*/
|
|
9
|
+
export type Call = (this: undefined, signal: AbortSignal) => void | Promise<void>
|
|
10
|
+
|
|
11
|
+
/** Flag types for an {@link Executable} */
|
|
12
|
+
export type Flag = 'skip' | 'only' | undefined
|
|
13
|
+
|
|
14
|
+
/** An {@link Executor} notifying lifecycle events for {@link Executable}s */
|
|
15
|
+
export interface Executor {
|
|
16
|
+
start(executable: Suite | Spec | Hook): {
|
|
17
|
+
notify(error: Error): void,
|
|
18
|
+
done(skip?: boolean): void,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ========================================================================== */
|
|
23
|
+
|
|
24
|
+
/** Execute a {@link Call} invoking the {@link Done} */
|
|
25
|
+
function execute(
|
|
26
|
+
call: Call,
|
|
27
|
+
timeout: number,
|
|
28
|
+
notify?: (error: Error) => void,
|
|
29
|
+
): Promise<Error | undefined> {
|
|
30
|
+
return new Promise<Error | undefined>((resolve) => {
|
|
31
|
+
let resolved = false
|
|
32
|
+
|
|
33
|
+
/* Create the abort controller */
|
|
34
|
+
const abort = new AbortController()
|
|
35
|
+
const handle = setTimeout(() => {
|
|
36
|
+
/* coverage ignore if */
|
|
37
|
+
if (resolved) return
|
|
38
|
+
|
|
39
|
+
const error = new Error(`Timeout of ${timeout} ms reached`)
|
|
40
|
+
resolve(error)
|
|
41
|
+
notify?.(error)
|
|
42
|
+
resolved = true
|
|
43
|
+
}, timeout).unref()
|
|
44
|
+
|
|
45
|
+
/* Use a secondary promise to wrap the (possibly async) call */
|
|
46
|
+
void Promise.resolve().then(async () => {
|
|
47
|
+
try {
|
|
48
|
+
await call.call(undefined, abort.signal)
|
|
49
|
+
resolve(undefined)
|
|
50
|
+
resolved = true
|
|
51
|
+
} catch (cause: any) {
|
|
52
|
+
const error = cause instanceof Error ? cause : new Error(String(cause))
|
|
53
|
+
notify?.(error)
|
|
54
|
+
resolve(error)
|
|
55
|
+
resolved = true
|
|
56
|
+
} finally {
|
|
57
|
+
abort.abort('Spec finished')
|
|
58
|
+
clearTimeout(handle)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ========================================================================== */
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
* Suite and skip storages must be unique _per process_. We might get called
|
|
68
|
+
* from two (or three) different versions of this file: the .cjs transpiled one,
|
|
69
|
+
* the .mjs transpiled one (or the .ts dynamically transpiled by ts-loader).
|
|
70
|
+
* In all these cases, we must return the _same_ object, so we store those as
|
|
71
|
+
* a global variables associated with a couple of global symbols
|
|
72
|
+
*/
|
|
73
|
+
const suiteKey = Symbol.for('plugjs.expect5.async.suiteStorage')
|
|
74
|
+
const skipKey = Symbol.for('plugjs.expect5.async.skipStorage')
|
|
75
|
+
|
|
76
|
+
function getSuiteStorage(): AsyncLocalStorage<Suite> {
|
|
77
|
+
let storage: AsyncLocalStorage<Suite> = (<any> globalThis)[suiteKey]
|
|
78
|
+
if (! storage) {
|
|
79
|
+
storage = new AsyncLocalStorage<Suite>()
|
|
80
|
+
;(<any> globalThis)[suiteKey] = storage
|
|
81
|
+
}
|
|
82
|
+
return storage
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getSkipStorage(): AsyncLocalStorage<{ skipped: boolean }> {
|
|
86
|
+
let storage: AsyncLocalStorage<{ skipped: boolean }> = (<any> globalThis)[skipKey]
|
|
87
|
+
if (! storage) {
|
|
88
|
+
storage = new AsyncLocalStorage<{ skipped: boolean }>()
|
|
89
|
+
;(<any> globalThis)[skipKey] = storage
|
|
90
|
+
}
|
|
91
|
+
return storage
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const suiteStorage = getSuiteStorage()
|
|
95
|
+
const skipStorage = getSkipStorage()
|
|
96
|
+
|
|
97
|
+
export function getCurrentSuite(): Suite {
|
|
98
|
+
const suite = suiteStorage.getStore()
|
|
99
|
+
assert(suite, 'No suite found')
|
|
100
|
+
return suite
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function skip(): void {
|
|
104
|
+
const skipState = skipStorage.getStore()
|
|
105
|
+
assert(skipState, 'The "skip" function can only be used in specs or hooks')
|
|
106
|
+
skipState.skipped = true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ========================================================================== */
|
|
110
|
+
|
|
111
|
+
/** A symbol marking {@link Suite} instances */
|
|
112
|
+
const suiteMarker = Symbol.for('plugjs:expect5:marker:Suite')
|
|
113
|
+
|
|
114
|
+
/** Our {@link Suite} implementation */
|
|
115
|
+
export class Suite {
|
|
116
|
+
private _beforeAll: Hook[] = []
|
|
117
|
+
private _beforeEach: Hook[] = []
|
|
118
|
+
private _afterAll: Hook[] = []
|
|
119
|
+
private _afterEach: Hook[] = []
|
|
120
|
+
private _suites: Suite[] = []
|
|
121
|
+
private _specs: Spec[] = []
|
|
122
|
+
private _children: (Suite | Spec)[] = []
|
|
123
|
+
private _setup: boolean = false
|
|
124
|
+
|
|
125
|
+
constructor(
|
|
126
|
+
public readonly parent: Suite | undefined,
|
|
127
|
+
public readonly name: string,
|
|
128
|
+
public readonly call: Call,
|
|
129
|
+
public readonly timeout: number = 5000,
|
|
130
|
+
public flag: Flag = undefined,
|
|
131
|
+
) {}
|
|
132
|
+
|
|
133
|
+
static {
|
|
134
|
+
(this.prototype as any)[suiteMarker] = suiteMarker
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static [Symbol.hasInstance](instance: any): boolean {
|
|
138
|
+
return instance && instance[suiteMarker] === suiteMarker
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get specs(): number {
|
|
142
|
+
return this._suites.reduce((n, s) => n + s.specs, 0) + this._specs.length
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Add a child {@link Suite} to this */
|
|
146
|
+
addSuite(suite: Suite): void {
|
|
147
|
+
assert.strictEqual(suite.parent, this, 'Suite is not a child of this')
|
|
148
|
+
this._children.push(suite)
|
|
149
|
+
this._suites.push(suite)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Add a {@link Spec} to this */
|
|
153
|
+
addSpec(spec: Spec): void {
|
|
154
|
+
assert.strictEqual(spec.parent, this, 'Spec is not a child of this')
|
|
155
|
+
this._children.push(spec)
|
|
156
|
+
this._specs.push(spec)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Add a _before all_ {@link Hook} to this */
|
|
160
|
+
addBeforeAllHook(hook: Hook): void {
|
|
161
|
+
assert.strictEqual(hook.parent, this, 'Hook is not a child of this')
|
|
162
|
+
assert.strictEqual(hook.name, 'beforeAll', `Invalid before all hook name "${hook.name}"`)
|
|
163
|
+
this._beforeAll.push(hook)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Add a _before each_ {@link Hook} to this */
|
|
167
|
+
addBeforeEachHook(hook: Hook): void {
|
|
168
|
+
assert.strictEqual(hook.parent, this, 'Hook is not a child of this')
|
|
169
|
+
assert.strictEqual(hook.name, 'beforeEach', `Invalid before each hook name "${hook.name}"`)
|
|
170
|
+
this._beforeEach.push(hook)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Add a _after all_ {@link Hook} to this */
|
|
174
|
+
addAfterAllHook(hook: Hook): void {
|
|
175
|
+
assert.strictEqual(hook.parent, this, 'Hook is not a child of this')
|
|
176
|
+
assert.strictEqual(hook.name, 'afterAll', `Invalid after all hook name "${hook.name}"`)
|
|
177
|
+
this._afterAll.push(hook)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Add a _after each_ {@link Hook} to this */
|
|
181
|
+
addAfterEachHook(hook: Hook): void {
|
|
182
|
+
assert.strictEqual(hook.parent, this, 'Hook is not a child of this')
|
|
183
|
+
assert.strictEqual(hook.name, 'afterEach', `Invalid after each hook name "${hook.name}"`)
|
|
184
|
+
this._afterEach.push(hook)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Setup this {@link Suite} invoking its main function, then initializing all
|
|
189
|
+
* children {@link Suite Suites}, and finally normalizing execution flags.
|
|
190
|
+
*/
|
|
191
|
+
async setup(): Promise<void> {
|
|
192
|
+
/* If this suite was already setup, this becomes a no-op */
|
|
193
|
+
if (this._setup) return
|
|
194
|
+
|
|
195
|
+
/* Run the setup call */
|
|
196
|
+
this._setup = true
|
|
197
|
+
await suiteStorage.run(this, async () => {
|
|
198
|
+
const error = await execute(this.call, this.timeout)
|
|
199
|
+
if (error) throw error
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
/* Copy before and after hooks from parent */
|
|
203
|
+
if (this.parent) {
|
|
204
|
+
this._beforeEach.unshift(...this.parent._beforeEach.map((h) => h.clone(this)))
|
|
205
|
+
this._afterEach.push(...this.parent._afterEach.map((h) => h.clone(this)))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Setup all sub-suites of this instance */
|
|
209
|
+
for (const suite of this._suites) {
|
|
210
|
+
await suite.setup()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Setup all before/after hooks in the spec */
|
|
214
|
+
for (const spec of this._specs) {
|
|
215
|
+
spec.before.push(...this._beforeEach.map((h) => h.clone(spec)))
|
|
216
|
+
spec.after.push(...this._afterEach.map((h) => h.clone(spec)))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* If _any_ of this suite's children is marked as "only", then all children
|
|
220
|
+
* not marked as such will be skipped, and this suite will also be marked
|
|
221
|
+
* as "only" (to inform parent suites) */
|
|
222
|
+
const only = this._children.reduce((o, c) => o || (c.flag === 'only'), false)
|
|
223
|
+
if (only) {
|
|
224
|
+
this._children.forEach((c) => (c.flag !== 'only') && (c.flag = 'skip'))
|
|
225
|
+
this.flag = 'only'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* If _this_ suite is marked as only, any child not marked with "skip" will
|
|
229
|
+
* be marked as "only" and included in the execution */
|
|
230
|
+
if (this.flag === 'only') {
|
|
231
|
+
this._children.forEach((c) => (c.flag !== 'skip') && (c.flag = 'only'))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* If all children are skipped, then this instance is skipped, too */
|
|
235
|
+
for (const child of this._children) {
|
|
236
|
+
if (child.flag !== 'skip') return
|
|
237
|
+
}
|
|
238
|
+
this.flag = 'skip'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Execute this suite, executing all {@link Hook hooks} and children
|
|
243
|
+
* {@link Spec specs} and {@link Suite suites}
|
|
244
|
+
*/
|
|
245
|
+
async execute(executor: Executor, skip: boolean = false): Promise<Error | void> {
|
|
246
|
+
const { done } = executor.start(this)
|
|
247
|
+
|
|
248
|
+
/* Potentially skip this (and all children) */
|
|
249
|
+
if (skip || (this.flag === 'skip')) {
|
|
250
|
+
for (const child of this._children) await child.execute(executor, true)
|
|
251
|
+
return done()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* Execute all our _before all_ hooks */
|
|
255
|
+
for (const hook of this._beforeAll) {
|
|
256
|
+
const failed = await hook.execute(executor)
|
|
257
|
+
/* Skip this (and all children on) _before all_ failure */
|
|
258
|
+
if (failed) {
|
|
259
|
+
for (const child of this._children) await child.execute(executor, true)
|
|
260
|
+
return done()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Execute all our children (specs or suites) */
|
|
265
|
+
for (const child of this._children) await child.execute(executor)
|
|
266
|
+
|
|
267
|
+
/* Execute all our _after all_ hooks (regardless of failures) */
|
|
268
|
+
for (const hook of this._afterAll) await hook.execute(executor)
|
|
269
|
+
|
|
270
|
+
/* Done */
|
|
271
|
+
done()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ========================================================================== */
|
|
276
|
+
|
|
277
|
+
/** A symbol marking {@link Spec} instances */
|
|
278
|
+
const specMarker = Symbol.for('plugjs:expect5:marker:Spec')
|
|
279
|
+
|
|
280
|
+
/** Our {@link Spec} implementation */
|
|
281
|
+
export class Spec {
|
|
282
|
+
public before: Hook[] = []
|
|
283
|
+
public after: Hook[] = []
|
|
284
|
+
|
|
285
|
+
constructor(
|
|
286
|
+
public readonly parent: Suite,
|
|
287
|
+
public readonly name: string,
|
|
288
|
+
public readonly call: Call,
|
|
289
|
+
public readonly timeout: number = 5000,
|
|
290
|
+
public flag: Flag = undefined,
|
|
291
|
+
) {}
|
|
292
|
+
|
|
293
|
+
static {
|
|
294
|
+
(this.prototype as any)[specMarker] = specMarker
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
static [Symbol.hasInstance](instance: any): boolean {
|
|
298
|
+
return instance && instance[specMarker] === specMarker
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Execute this spec */
|
|
302
|
+
async execute(executor: Executor, skip: boolean = false): Promise<void> {
|
|
303
|
+
const { done, notify } = executor.start(this)
|
|
304
|
+
|
|
305
|
+
/* Potentially skip this */
|
|
306
|
+
if (skip || (this.flag == 'skip')) return done(true)
|
|
307
|
+
|
|
308
|
+
/* Execute all our _before each_ hooks */
|
|
309
|
+
for (const hook of this.before) {
|
|
310
|
+
const failed = await hook.execute(executor)
|
|
311
|
+
if (failed) return done(true)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* Execute our spec */
|
|
315
|
+
const skipState = { skipped: false }
|
|
316
|
+
await skipStorage.run(skipState, () => execute(this.call, this.timeout, notify))
|
|
317
|
+
|
|
318
|
+
/* Execute all our _after all_ hooks (regardless of failures) */
|
|
319
|
+
for (const hook of this.after) await hook.execute(executor)
|
|
320
|
+
|
|
321
|
+
/* Done! */
|
|
322
|
+
return done(skipState.skipped)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* ========================================================================== */
|
|
327
|
+
|
|
328
|
+
/** A symbol marking {@link Hook} instances */
|
|
329
|
+
const hookMarker = Symbol.for('plugjs:expect5:marker:Hook')
|
|
330
|
+
|
|
331
|
+
/** Our {@link Hook} implementation */
|
|
332
|
+
export class Hook {
|
|
333
|
+
constructor(
|
|
334
|
+
public readonly parent: Suite | Spec,
|
|
335
|
+
public readonly name: 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach',
|
|
336
|
+
public readonly call: Call,
|
|
337
|
+
public readonly timeout: number = 5000,
|
|
338
|
+
public readonly flag: Exclude<Flag, 'only'> = undefined,
|
|
339
|
+
) {}
|
|
340
|
+
|
|
341
|
+
static {
|
|
342
|
+
(this.prototype as any)[hookMarker] = hookMarker
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
static [Symbol.hasInstance](instance: any): boolean {
|
|
346
|
+
return instance && instance[hookMarker] === hookMarker
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Execute this hook */
|
|
350
|
+
async execute(executor: Executor): Promise<boolean> {
|
|
351
|
+
if (this.flag === 'skip') return false
|
|
352
|
+
const { done, notify } = executor.start(this)
|
|
353
|
+
|
|
354
|
+
const skipState = { skipped: false }
|
|
355
|
+
const error = await skipStorage.run(skipState, () => execute(this.call, this.timeout, notify))
|
|
356
|
+
done(skipState.skipped)
|
|
357
|
+
return !! error
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Clone this associating it with a new {@link Suite} or {@link Spec} */
|
|
361
|
+
clone(parent: Suite | Spec): Hook {
|
|
362
|
+
return new Hook(parent, this.name, this.call, this.timeout, this.flag)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
import { Spec, Suite, Hook } from './executable'
|
|
5
|
+
|
|
6
|
+
import type { Executor } from './executable'
|
|
7
|
+
|
|
8
|
+
/* ========================================================================== */
|
|
9
|
+
|
|
10
|
+
export interface ExecutionFailure {
|
|
11
|
+
number: number,
|
|
12
|
+
error: Error,
|
|
13
|
+
source: Suite | Spec | Hook,
|
|
14
|
+
type: 'suite' | 'spec' | 'hook',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExecutionEvents {
|
|
18
|
+
'suite:start': (suite: Suite) => void
|
|
19
|
+
'suite:done': (suite: Suite, time: number) => void
|
|
20
|
+
|
|
21
|
+
'spec:start': (spec: Spec) => void
|
|
22
|
+
'spec:error': (spec: Spec, failure: ExecutionFailure) => void
|
|
23
|
+
'spec:skip': (spec: Spec, time: number) => void
|
|
24
|
+
'spec:pass': (spec: Spec, time: number) => void
|
|
25
|
+
'spec:fail': (spec: Spec, time: number, failure: ExecutionFailure) => void
|
|
26
|
+
|
|
27
|
+
'hook:start': (hook: Hook) => void
|
|
28
|
+
'hook:error': (hook: Hook, failure: ExecutionFailure) => void
|
|
29
|
+
'hook:skip': (hook: Hook, time: number) => void
|
|
30
|
+
'hook:pass': (hook: Hook, time: number) => void
|
|
31
|
+
'hook:fail': (hook: Hook, time: number, failure: ExecutionFailure) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Execution {
|
|
35
|
+
on<E extends keyof ExecutionEvents>(event: E, listener: ExecutionEvents[E]): this
|
|
36
|
+
off<E extends keyof ExecutionEvents>(event: E, listener: ExecutionEvents[E]): this
|
|
37
|
+
once<E extends keyof ExecutionEvents>(event: E, listener: ExecutionEvents[E]): this
|
|
38
|
+
result: Promise<ExecutionResult>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ExecutionResult {
|
|
42
|
+
passed: number
|
|
43
|
+
failed: number
|
|
44
|
+
skipped: number
|
|
45
|
+
time: number
|
|
46
|
+
failures: ExecutionFailure[],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ========================================================================== */
|
|
50
|
+
|
|
51
|
+
export function runSuite(suite: Suite): Execution {
|
|
52
|
+
const _emitter = new EventEmitter()
|
|
53
|
+
|
|
54
|
+
let resolve: (value: ExecutionResult | PromiseLike<ExecutionResult>) => void
|
|
55
|
+
let reject: (reason?: any) => void
|
|
56
|
+
const promise = new Promise<ExecutionResult>((_resolve, _reject) => {
|
|
57
|
+
resolve = _resolve
|
|
58
|
+
reject = _reject
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const result: ExecutionResult = {
|
|
62
|
+
time: 0,
|
|
63
|
+
passed: 0,
|
|
64
|
+
failed: 0,
|
|
65
|
+
skipped: 0,
|
|
66
|
+
failures: [],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const execution: Execution = {
|
|
70
|
+
result: promise,
|
|
71
|
+
on: (event: string, listener: (...args: any[]) => void): Execution => {
|
|
72
|
+
_emitter.on(event, listener)
|
|
73
|
+
return execution
|
|
74
|
+
},
|
|
75
|
+
off: (event: string, listener: (...args: any[]) => void): Execution => {
|
|
76
|
+
_emitter.off(event, listener)
|
|
77
|
+
return execution
|
|
78
|
+
},
|
|
79
|
+
once: (event: string, listener: (...args: any[]) => void): Execution => {
|
|
80
|
+
_emitter.once(event, listener)
|
|
81
|
+
return execution
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const start = (executable: Suite | Spec | Hook): ReturnType<Executor['start']> => {
|
|
86
|
+
const type: 'suite' | 'spec' | 'hook' =
|
|
87
|
+
executable instanceof Suite ? 'suite' :
|
|
88
|
+
executable instanceof Spec ? 'spec' :
|
|
89
|
+
executable instanceof Hook ? 'hook' :
|
|
90
|
+
/* coverage ignore next */
|
|
91
|
+
assert.fail(`Unable to start ${Object.getPrototypeOf(executable)?.constructor?.name}`)
|
|
92
|
+
|
|
93
|
+
const now = Date.now()
|
|
94
|
+
_emitter.emit(`${type}:start`, executable)
|
|
95
|
+
|
|
96
|
+
let done = false
|
|
97
|
+
let failure: ExecutionFailure | undefined
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
done(skipped: boolean = false): void {
|
|
101
|
+
const time = Date.now() - now
|
|
102
|
+
done = true
|
|
103
|
+
|
|
104
|
+
if (type === 'suite') {
|
|
105
|
+
_emitter.emit(`${type}:done`, executable, time)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (failure) {
|
|
110
|
+
_emitter.emit(`${type}:fail`, executable, time, failure)
|
|
111
|
+
if (type === 'spec') result.failed ++
|
|
112
|
+
} else if (skipped) {
|
|
113
|
+
_emitter.emit(`${type}:skip`, executable, time)
|
|
114
|
+
if (type === 'spec') result.skipped ++
|
|
115
|
+
} else {
|
|
116
|
+
_emitter.emit(`${type}:pass`, executable, time)
|
|
117
|
+
if (type === 'spec') result.passed ++
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
notify(error: Error): void {
|
|
121
|
+
const number = result.failures.length + 1
|
|
122
|
+
const fail = { error, number, source: executable, type }
|
|
123
|
+
result.failures.push(fail)
|
|
124
|
+
|
|
125
|
+
// notify error after done, or include in failure?
|
|
126
|
+
if (failure || done) {
|
|
127
|
+
_emitter.emit(`${type}:error`, executable, fail)
|
|
128
|
+
} else {
|
|
129
|
+
failure = fail
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setImmediate(() => Promise.resolve().then(async () => {
|
|
136
|
+
const now = Date.now()
|
|
137
|
+
await suite.setup()
|
|
138
|
+
await suite.execute({ start })
|
|
139
|
+
|
|
140
|
+
result.time = Date.now() - now
|
|
141
|
+
|
|
142
|
+
resolve(result)
|
|
143
|
+
}).catch((error) => reject(error)))
|
|
144
|
+
|
|
145
|
+
return execution
|
|
146
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { getCurrentSuite, Suite, Spec, Hook } from './executable'
|
|
2
|
+
|
|
3
|
+
import type { Call } from './executable'
|
|
4
|
+
|
|
5
|
+
export type SuiteSetup = (name: string, call: Call, timeout?: number) => void
|
|
6
|
+
export type SuiteFunction = SuiteSetup & {
|
|
7
|
+
only: SuiteSetup,
|
|
8
|
+
skip: SuiteSetup,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const describe: SuiteFunction = (name: string, call: Call, timeout?: number): void => {
|
|
12
|
+
const parent = getCurrentSuite()
|
|
13
|
+
parent.addSuite(new Suite(parent, name, call, timeout))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const fdescribe: SuiteSetup = (name: string, call: Call, timeout?: number): void => {
|
|
17
|
+
const parent = getCurrentSuite()
|
|
18
|
+
parent.addSuite(new Suite(parent, name, call, timeout, 'only'))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const xdescribe: SuiteSetup = (name: string, call: Call, timeout?: number): void => {
|
|
22
|
+
const parent = getCurrentSuite()
|
|
23
|
+
parent.addSuite(new Suite(parent, name, call, timeout, 'skip'))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe.skip = xdescribe
|
|
27
|
+
describe.only = fdescribe
|
|
28
|
+
|
|
29
|
+
/* ========================================================================== */
|
|
30
|
+
|
|
31
|
+
export type SpecSetup = (name: string, call: Call, timeout?: number) => void
|
|
32
|
+
export type SpecFunction = SpecSetup & {
|
|
33
|
+
only: SpecSetup,
|
|
34
|
+
skip: SpecSetup,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const it: SpecFunction = (name: string, call: Call, timeout?: number): void => {
|
|
38
|
+
const parent = getCurrentSuite()
|
|
39
|
+
parent.addSpec(new Spec(parent, name, call, timeout))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const fit: SpecSetup = (name: string, call: Call, timeout?: number): void => {
|
|
43
|
+
const parent = getCurrentSuite()
|
|
44
|
+
parent.addSpec(new Spec(parent, name, call, timeout, 'only'))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const xit: SpecSetup = (name: string, call: Call, timeout?: number): void => {
|
|
48
|
+
const parent = getCurrentSuite()
|
|
49
|
+
parent.addSpec(new Spec(parent, name, call, timeout, 'skip'))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
it.skip = xit
|
|
53
|
+
it.only = fit
|
|
54
|
+
|
|
55
|
+
/* ========================================================================== */
|
|
56
|
+
|
|
57
|
+
export type HookSetup = (call: Call, timeout?: number) => void
|
|
58
|
+
export type HookFunction = HookSetup & {
|
|
59
|
+
skip: HookSetup,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const beforeAll: HookFunction = (call: Call, timeout?: number): void => {
|
|
63
|
+
const parent = getCurrentSuite()
|
|
64
|
+
parent.addBeforeAllHook(new Hook(parent, 'beforeAll', call, timeout))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const xbeforeAll: HookSetup = (call: Call, timeout?: number): void => {
|
|
68
|
+
const parent = getCurrentSuite()
|
|
69
|
+
parent.addBeforeAllHook(new Hook(parent, 'beforeAll', call, timeout, 'skip'))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const beforeEach: HookFunction = (call: Call, timeout?: number): void => {
|
|
73
|
+
const parent = getCurrentSuite()
|
|
74
|
+
parent.addBeforeEachHook(new Hook(parent, 'beforeEach', call, timeout))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const xbeforeEach: HookSetup = (call: Call, timeout?: number): void => {
|
|
78
|
+
const parent = getCurrentSuite()
|
|
79
|
+
parent.addBeforeEachHook(new Hook(parent, 'beforeEach', call, timeout, 'skip'))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const afterAll: HookFunction = (call: Call, timeout?: number): void => {
|
|
83
|
+
const parent = getCurrentSuite()
|
|
84
|
+
parent.addAfterAllHook(new Hook(parent, 'afterAll', call, timeout))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const xafterAll: HookSetup = (call: Call, timeout?: number): void => {
|
|
88
|
+
const parent = getCurrentSuite()
|
|
89
|
+
parent.addAfterAllHook(new Hook(parent, 'afterAll', call, timeout, 'skip'))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const afterEach: HookFunction = (call: Call, timeout?: number): void => {
|
|
93
|
+
const parent = getCurrentSuite()
|
|
94
|
+
parent.addAfterEachHook(new Hook(parent, 'afterEach', call, timeout))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const xafterEach: HookSetup = (call: Call, timeout?: number): void => {
|
|
98
|
+
const parent = getCurrentSuite()
|
|
99
|
+
parent.addAfterEachHook(new Hook(parent, 'afterEach', call, timeout, 'skip'))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
beforeAll.skip = xbeforeAll
|
|
103
|
+
beforeEach.skip = xbeforeEach
|
|
104
|
+
afterAll.skip = xafterAll
|
|
105
|
+
afterEach.skip = xafterEach
|
|
106
|
+
|
|
107
|
+
export const before: HookSetup = beforeAll
|
|
108
|
+
export const after: HookSetup = afterAll
|