UNPKG

5.15 kBJavaScriptView Raw
1import kleur from 'kleur';
2import { compare } from 'uvu/diff';
3
4let isCLI = false, isNode = false;
5let hrtime = (now = Date.now()) => () => (Date.now() - now).toFixed(2) + 'ms';
6let write = console.log;
7
8const into = (ctx, key) => (name, handler) => ctx[key].push({ name, handler });
9const context = (state) => ({ tests:[], before:[], after:[], bEach:[], aEach:[], only:[], skips:0, state });
10const milli = arr => (arr[0]*1e3 + arr[1]/1e6).toFixed(2) + 'ms';
11const hook = (ctx, key) => handler => ctx[key].push(handler);
12
13if (isNode = typeof process < 'u' && typeof process.stdout < 'u') {
14 // globalThis polyfill; Node < 12
15 if (typeof globalThis !== 'object') {
16 Object.defineProperty(global, 'globalThis', {
17 get: function () { return this }
18 });
19 }
20
21 let rgx = /(\.bin[\\+\/]uvu$|uvu[\\+\/]bin\.js)/i;
22 isCLI = process.argv.some(x => rgx.test(x));
23
24 // attach node-specific utils
25 write = x => process.stdout.write(x);
26 hrtime = (now = process.hrtime()) => () => milli(process.hrtime(now));
27} else if (typeof performance < 'u') {
28 hrtime = (now = performance.now()) => () => (performance.now() - now).toFixed(2) + 'ms';
29}
30
31globalThis.UVU_QUEUE = globalThis.UVU_QUEUE || [];
32isCLI = isCLI || !!globalThis.UVU_DEFER;
33isCLI || UVU_QUEUE.push([null]);
34
35const QUOTE = kleur.dim('"'), GUTTER = '\n ';
36const FAIL = kleur.red('✘ '), PASS = kleur.gray('• ');
37const IGNORE = /^\s*at.*(?:\(|\s)(?:node|(internal\/[\w/]*))/;
38const FAILURE = kleur.bold().bgRed(' FAIL ');
39const FILE = kleur.bold().underline().white;
40const SUITE = kleur.bgWhite().bold;
41
42function stack(stack, idx) {
43 let i=0, line, out='';
44 let arr = stack.substring(idx).replace(/\\/g, '/').split('\n');
45 for (; i < arr.length; i++) {
46 line = arr[i].trim();
47 if (line.length && !IGNORE.test(line)) {
48 out += '\n ' + line;
49 }
50 }
51 return kleur.grey(out) + '\n';
52}
53
54function format(name, err, suite = '') {
55 let { details, operator='' } = err;
56 let idx = err.stack && err.stack.indexOf('\n');
57 if (err.name.startsWith('AssertionError') && !operator.includes('not')) details = compare(err.actual, err.expected); // TODO?
58 let str = ' ' + FAILURE + (suite ? kleur.red(SUITE(` ${suite} `)) : '') + ' ' + QUOTE + kleur.red().bold(name) + QUOTE;
59 str += '\n ' + err.message + (operator ? kleur.italic().dim(` (${operator})`) : '') + '\n';
60 if (details) str += GUTTER + details.split('\n').join(GUTTER);
61 if (!!~idx) str += stack(err.stack, idx);
62 return str + '\n';
63}
64
65async function runner(ctx, name) {
66 let { only, tests, before, after, bEach, aEach, state } = ctx;
67 let hook, test, arr = only.length ? only : tests;
68 let num=0, errors='', total=arr.length;
69
70 try {
71 if (name) write(SUITE(kleur.black(` ${name} `)) + ' ');
72 for (hook of before) await hook(state);
73
74 for (test of arr) {
75 state.__test__ = test.name;
76 try {
77 for (hook of bEach) await hook(state);
78 await test.handler(state);
79 for (hook of aEach) await hook(state);
80 write(PASS);
81 num++;
82 } catch (err) {
83 for (hook of aEach) await hook(state);
84 if (errors.length) errors += '\n';
85 errors += format(test.name, err, name);
86 write(FAIL);
87 }
88 }
89 } finally {
90 state.__test__ = '';
91 for (hook of after) await hook(state);
92 let msg = ` (${num} / ${total})\n`;
93 let skipped = (only.length ? tests.length : 0) + ctx.skips;
94 write(errors.length ? kleur.red(msg) : kleur.green(msg));
95 return [errors || true, num, skipped, total];
96 }
97}
98
99let timer;
100function defer() {
101 clearTimeout(timer);
102 timer = setTimeout(exec);
103}
104
105function setup(ctx, name = '') {
106 ctx.state.__test__ = '';
107 ctx.state.__suite__ = name;
108 const test = into(ctx, 'tests');
109 test.before = hook(ctx, 'before');
110 test.before.each = hook(ctx, 'bEach');
111 test.after = hook(ctx, 'after');
112 test.after.each = hook(ctx, 'aEach');
113 test.only = into(ctx, 'only');
114 test.skip = () => { ctx.skips++ };
115 test.run = () => {
116 let copy = { ...ctx };
117 let run = runner.bind(0, copy, name);
118 Object.assign(ctx, context(copy.state));
119 UVU_QUEUE[globalThis.UVU_INDEX || 0].push(run);
120 isCLI || defer();
121 };
122 return test;
123}
124
125export const suite = (name = '', state = {}) => setup(context(state), name);
126export const test = suite();
127
128let isRunning = false;
129export async function exec(bail) {
130 let timer = hrtime();
131 let done=0, total=0, skips=0, code=0;
132
133 isRunning = true;
134 for (let group of UVU_QUEUE) {
135 if (total) write('\n');
136
137 let name = group.shift();
138 if (name != null) write(FILE(name) + '\n');
139
140 for (let test of group) {
141 let [errs, ran, skip, max] = await test();
142 total += max; done += ran; skips += skip;
143 if (errs.length) {
144 write('\n' + errs + '\n'); code=1;
145 if (bail) return isNode && process.exit(1);
146 }
147 }
148 }
149
150 isRunning = false;
151 write('\n Total: ' + total);
152 write((code ? kleur.red : kleur.green)('\n Passed: ' + done));
153 write('\n Skipped: ' + (skips ? kleur.yellow(skips) : skips));
154 write('\n Duration: ' + timer() + '\n\n');
155
156 if (isNode) process.exitCode = code;
157}
158
159if (isNode) process.on('exit', () => {
160 if (!isRunning) return; // okay to exit
161 process.exitCode = process.exitCode || 1;
162 console.error('Exiting early before testing is finished.');
163});