1 | process.setMaxListeners(0)
|
2 | process.once('beforeExit', next)
|
3 | const queue = []
|
4 | exports.test = (label, fn) => push({label, fn})
|
5 | exports.test.skip = (label, fn, doSkip = true) => push({
|
6 | label,
|
7 | fn: doSkip ? Function.prototype : fn
|
8 | })
|
9 | exports.test.timeout = (label, fn, ms, doTimeout = true) => push({
|
10 | label,
|
11 | fn (done, timeoutError = null) {
|
12 | try {
|
13 | const error = new Error(`TimeoutError: ${ms}ms exceeded.`)
|
14 | const timeout = setTimeout(doTimeout ? done : Function.prototype, ms, error)
|
15 | fn((err) => {
|
16 | clearTimeout(timeout)
|
17 | if (!timeout._called || !doTimeout) done(err)
|
18 | })
|
19 | if (fn.length === 0) done(null)
|
20 | } catch (err) {
|
21 | done(err)
|
22 | }
|
23 | }
|
24 | })
|
25 | exports.beforeEach = (before, {assign} = Object) => {
|
26 | queue.map(context => {
|
27 | const fn = context.fn
|
28 | return assign(context, {
|
29 | fn (done) {
|
30 | try {
|
31 | before()
|
32 | fn(done)
|
33 | if (fn.length === 0) done(null)
|
34 | } catch (err) {
|
35 | done(err)
|
36 | }
|
37 | }
|
38 | })
|
39 | })
|
40 | }
|
41 | exports.afterEach = (after, {assign} = Object) => {
|
42 | queue.map(context => {
|
43 | const fn = context.fn
|
44 | return assign(context, {
|
45 | fn (done) {
|
46 | try {
|
47 | fn((err) => {
|
48 | try {
|
49 | after(() => done(err))
|
50 | if (after.length === 0) done(err)
|
51 | } catch (err) {
|
52 | done(err)
|
53 | }
|
54 | })
|
55 | if (fn.length === 0) {
|
56 | try {
|
57 | after(done)
|
58 | if (after.length === 0) done(null)
|
59 | } catch (err) {
|
60 | done(err)
|
61 | }
|
62 | }
|
63 | } catch (err) {
|
64 | done(err)
|
65 | }
|
66 | }
|
67 | })
|
68 | })
|
69 | }
|
70 | function push () {
|
71 | queue.push(...arguments)
|
72 | }
|
73 | function next () {
|
74 | if (queue.length === 0) return
|
75 | const {fn, done} = shift(queue)
|
76 | const handle = trap(done)
|
77 | try {
|
78 | queue.length = 0
|
79 | fn(handle)
|
80 | if (fn.length === 0) handle(null)
|
81 | } catch (err) {
|
82 | handle(err)
|
83 | }
|
84 | function trap (done) {
|
85 | process.once('uncaughtException', done)
|
86 | return (err = null) => {
|
87 | process.removeListener('uncaughtException', done)
|
88 | done(err)
|
89 | }
|
90 | }
|
91 | }
|
92 | function shift ([context, ...pending]) {
|
93 | const {elapsed} = timerFor(context)
|
94 | return {
|
95 | done (err) {
|
96 | const indent = indentFor(context)
|
97 | if (queue.length > 0) {
|
98 | console.log('%s%s', indent, context.label)
|
99 | } else {
|
100 | if (err) {
|
101 | process.exitCode = 1
|
102 | console.log('%s\x1b[31m✘\x1b[0m %s (%dms)', indent, context.label, elapsed())
|
103 | if (err.name) {
|
104 | console.log(' %s\x1b[31m%s\x1b[0m', indent, err.name, err.message)
|
105 | console.error(err.stack.toString().split('\n').splice(1).join('\n'))
|
106 | } else {
|
107 | console.log(' %s%s', indent, err)
|
108 | }
|
109 | } else {
|
110 | if (context.fn === Function.prototype) {
|
111 | console.log('%s- %s', indent, context.label)
|
112 | } else {
|
113 | console.log('%s\x1b[32m✔\x1b[0m %s (%dms)', indent, context.label, elapsed())
|
114 | }
|
115 | }
|
116 | }
|
117 | queue.forEach(bindTo(context))
|
118 | push(...pending)
|
119 | next(err)
|
120 | },
|
121 | fn: context.fn
|
122 | }
|
123 | }
|
124 | function indentFor (context, length = 0) {
|
125 | if (context.parent === undefined) {
|
126 | return Array.from({length}).fill(' ').join('')
|
127 | }
|
128 | return indentFor(context.parent, ++length)
|
129 | }
|
130 | function bindTo (parent, {assign} = Object) {
|
131 | return (context) => assign(context, {parent})
|
132 | }
|
133 | function timerFor (context, initial = Date.now()) {
|
134 | return {
|
135 | elapsed () {
|
136 | return Date.now() - initial
|
137 | }
|
138 | }
|
139 | }
|