1 | import kleur from 'kleur';
|
2 | import { compare } from 'uvu/diff';
|
3 |
|
4 | let isCLI = false, isNode = false;
|
5 | let hrtime = (now = Date.now()) => () => (Date.now() - now).toFixed(2) + 'ms';
|
6 | let write = console.log;
|
7 |
|
8 | const into = (ctx, key) => (name, handler) => ctx[key].push({ name, handler });
|
9 | const context = (state) => ({ tests:[], before:[], after:[], bEach:[], aEach:[], only:[], skips:0, state });
|
10 | const milli = arr => (arr[0]*1e3 + arr[1]/1e6).toFixed(2) + 'ms';
|
11 | const hook = (ctx, key) => handler => ctx[key].push(handler);
|
12 |
|
13 | if (isNode = typeof process < 'u' && typeof process.stdout < 'u') {
|
14 |
|
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 |
|
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 |
|
31 | globalThis.UVU_QUEUE = globalThis.UVU_QUEUE || [];
|
32 | isCLI = isCLI || !!globalThis.UVU_DEFER;
|
33 | isCLI || UVU_QUEUE.push([null]);
|
34 |
|
35 | const QUOTE = kleur.dim('"'), GUTTER = '\n ';
|
36 | const FAIL = kleur.red('✘ '), PASS = kleur.gray('• ');
|
37 | const IGNORE = /^\s*at.*(?:\(|\s)(?:node|(internal\/[\w/]*))/;
|
38 | const FAILURE = kleur.bold().bgRed(' FAIL ');
|
39 | const FILE = kleur.bold().underline().white;
|
40 | const SUITE = kleur.bgWhite().bold;
|
41 |
|
42 | function 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 |
|
54 | function 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);
|
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 |
|
65 | async 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 |
|
99 | let timer;
|
100 | function defer() {
|
101 | clearTimeout(timer);
|
102 | timer = setTimeout(exec);
|
103 | }
|
104 |
|
105 | function 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 |
|
125 | export const suite = (name = '', state = {}) => setup(context(state), name);
|
126 | export const test = suite();
|
127 |
|
128 | let isRunning = false;
|
129 | export 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 |
|
159 | if (isNode) process.on('exit', () => {
|
160 | if (!isRunning) return;
|
161 | process.exitCode = process.exitCode || 1;
|
162 | console.error('Exiting early before testing is finished.');
|
163 | });
|