-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapi.js
308 lines (283 loc) · 8.22 KB
/
api.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
import parallel from 'p-map';
import sequence from 'p-map-series';
import mixinDeep from 'mixin-deep';
import isObservable from 'is-observable';
import observable2promise from 'observable-to-promise';
import defaultReporter from './reporters/tap';
import { nextTick, noopReporter, hasProcess, importReporter } from './utils';
/**
* Constructor which can be initialized with optional `options` object.
* On the `.test` method you can access the `skip` and `todo` methods.
* For example `.test.skip(title, fn)` and `.test.todo(title)`.
*
* This should be uses if you want to base something on top of the Asia API.
* By default, the main export e.g. just `'asia'` exposes a default export function,
* which is the `test()` method.
*
* @example
* import Asia from 'asia/dist/api/es';
*
* // or in CommonJS (Node.js)
* // const Asia = require('asia/dist/api/umd');
*
* const api = Asia({ serial: true });
* console.log(api);
* // => { test() {}, skip() {}, todo() {}, run() {} }
*
* api.test('awesome test', async () => {
* await Promise.resolve(123);
* });
*
* api.test.skip('some skip test here', () => {
* console.log('this will not log');
* });
* api.skip('same as above', () => {
* console.log('this will not log');
* });
*
* api.test.todo('test without implementaton');
* api.todo('test without implementaton');
*
* api.run();
*
* @name Asia
* @param {object} options control tests `concurrency` or pass `serial: true` to run serially.
* @returns {object} instance with `.test`, `.skip`, `.todo` and `.run` methods
* @public
*/
export default function Asia(options) {
const stats = { count: 0, anonymous: 0, pass: 0, fail: 0, skip: 0, todo: 0 };
const opts = mixinDeep(
{
args: [],
stats,
relativePaths: true,
proc: /* istanbul ignore next */ hasProcess ? process : {},
serial:
/* istanbul ignore next */ hasProcess && process.env.ASIA_SERIAL
? Boolean(process.env.ASIA_SERIAL)
: false,
showStack:
/* istanbul ignore next */ hasProcess && process.env.ASIA_SHOW_STACK
? Boolean(process.env.ASIA_SHOW_STACK)
: false,
},
options,
);
const tests = [];
/**
* Define a regular test with `title` and `fn`.
* Both `title` and `fn` params are required, otherwise it will throw.
* Optionally you can pass `settings` options object, to make it a "skip"
* or a "todo" test. For example `{ skip: true }`
*
* @example
* import assert from 'assert';
* import expect from 'expect';
* import test from 'asia';
*
* test('some awesome failing test', () => {
* expect(1).toBe(2);
* });
*
* test('foo passing async test', async () => {
* const res = await Promise.resolve(123);
*
* assert.strictEqual(res, 123);
* });
*
* @name test
* @param {string} title
* @param {function} fn
* @param {object} settings
* @public
*/
function addTest(title, fn, settings) {
const opt = Object.assign({ skip: false, todo: false }, settings);
if (typeof title === 'function') {
fn = title;
opts.stats.anonymous += 1;
title = `anonymous test ${opts.stats.anonymous}`;
}
if (typeof title !== 'string') {
throw new TypeError('asia.test(): expect test `title` be string');
}
if (typeof fn !== 'function') {
throw new TypeError('asia.test(): expect test `fn` to be function');
}
opts.stats.count += 1;
const id = opts.stats.count;
if (opt.skip) opts.stats.skip += 1;
if (opt.todo) opts.stats.todo += 1;
Object.assign(fn, opt, { fn, stats: opts.stats, title, id, index: id });
tests.push(fn);
}
/**
* Define test with `title` and `fn` that will never run,
* but will be shown in the output.
*
* @example
* import test from 'asia';
*
* test.skip('should be skipped, but printed', () => {
* throw Error('test function never run');
* });
*
* test.skip('should throw, because expect test implementation');
*
* @name test.skip
* @param {string} title test title
* @param {function} fn test function implementaton
* @public
*/
function addSkipTest(title, fn) {
addTest(title, fn, { skip: true });
}
/**
* Define a test with `title` that will be marked as "todo" test.
* Such tests do not have test implementaton function, if `fn` is given
* than it will throw an error.
*
* @example
* import assert from 'assert';
* import test from 'asia';
*
* test.todo('should be printed and okey');
*
* test.todo('should throw, because does not expect test fn', () => {
* assert.ok(true);
* });
*
* @name test.todo
* @param {string} title title of the "todo" test
* @param {function} fn do not pass test implementaton function
* @public
*/
function addTodoTest(title, fn) {
if (typeof fn === 'function') {
throw new TypeError('asia.test.todo(): do NOT expect test `fn`');
}
/* istanbul ignore next */
const fakeFn = () => {};
addTest(title, fakeFn, { todo: true });
}
addTest.test = addTest;
addTest.skip = addSkipTest;
addTest.todo = addTodoTest;
return {
test: addTest,
skip: addSkipTest,
todo: addTodoTest,
run: createRun(tests, opts),
};
}
function createRun(tests, opts) {
/**
* Run all tests, with optional `settings` options, merged with those
* passed from the constructor.
* Currently the supported options are `serial` and `concurrency`.
*
* @example
* import delay from 'delay';
* import Asia from 'asia/dist/api/es';
*
* const api = Asia({ serial: true });
*
* api.test('first test', async () => {
* await delay(1000);
* console.log('one');
* });
*
* api.test('second test', () => {
* console.log('two');
* });
*
* api.run({ concurrency: 10 });
*
* @name .run
* @param {object} settings for example, pass `serial: true` to run the tests serially
* @return {Promise}
* @public
*/
return nextTick(async (settings) => {
const options = mixinDeep({}, opts, settings);
const flow = options.serial ? sequence : parallel;
const results = [];
let reporter = defaultReporter({ tests, options });
if (typeof options.reporter === 'function') {
reporter = Object.assign(
{},
noopReporter,
options.reporter({ tests, options }),
);
}
/* istanbul ignore next */
if (typeof options.reporter === 'string' && hasProcess) {
const reporterFn = await importReporter(options.reporter);
reporter = Object.assign(
{},
noopReporter,
reporterFn({ tests, options }),
);
}
/* istanbul ignore next */
if (typeof options.writeLine !== 'function') {
options.writeLine = console.log;
}
await reporter.before(options);
return flow(
tests,
(item) =>
// For each test -> run 3 steps, always serially
sequence(
[
// Step 1: Before each test
() => reporter.beforeEach(item, options),
// Step 2: Run the test function
stepTwo(item, options),
// Step 3: After each test function
() => reporter.afterEach(item, options),
],
(step) => step(),
).then((steps) => {
// Always get the middle step result
results.push(steps[1]);
}),
options,
).then(async () => {
await reporter.after(options, results);
return { options, results, tests };
});
});
}
async function stepTwo(item, options) {
return async () => {
if (!item.skip && !item.todo) {
const { value, reason } = await runTest(item, options.args);
item.value = value;
item.reason = reason;
if (item.reason) {
item.fail = true;
item.pass = false;
item.stats.fail += 1;
} else {
item.fail = false;
item.pass = true;
item.stats.pass += 1;
}
}
options.stats = Object.assign({}, options.stats, item.stats);
return item;
};
}
async function runTest(item, args) {
let value = null;
let reason = null;
try {
const val = await item.fn.apply(null, args);
value = isObservable(val) ? await observable2promise(val) : val;
} catch (err) {
reason = err;
}
return { value, reason };
}