|
1 | | -const rgb = require("barecolor"); |
2 | | - |
| 1 | +const path = require('node:path') |
3 | 2 | async function RunSerially(fnArray) { |
4 | | - for (const fn of fnArray) { |
5 | | - await fn(); |
6 | | - } |
| 3 | + for (const fn of fnArray) await fn() |
7 | 4 | } |
8 | | -const suiteStack = []; |
9 | | -let testRunPromise = Promise.resolve(); |
10 | | -const top = () => (suiteStack.length ? suiteStack[suiteStack.length - 1] : null); |
11 | | -const topSafe = () => (suiteStack.length ? suiteStack[suiteStack.length - 1] : makeSuite("")); |
12 | 5 |
|
13 | | -const registerTest = (suite, name, fn) => async () => { |
14 | | - const prefix = " ".repeat(suite.depth); |
| 6 | +async function* runTestSuite(options = {}) { |
| 7 | + const suite = top() |
| 8 | + const tests = (suite.tests.some((t) => t.only) ? suite.tests.filter((t) => t.only) : suite.tests).filter( |
| 9 | + (t) => !options.only?.length || options.only[0] === t.name |
| 10 | + ) |
15 | 11 | try { |
16 | | - await suite.beforeEach(); |
17 | | - await fn(); |
18 | | - rgb.green(prefix + `✓`); |
19 | | - rgb.gray(` ${name}\n`); |
20 | | - } catch (e) { |
21 | | - rgb.red(`\n${prefix}✗ ${name} \n\n`); |
22 | | - throw e; |
| 12 | + await RunSerially(suite.before) |
| 13 | + for (let index = 0; index < tests.length; index++) { |
| 14 | + const test = tests[index] |
| 15 | + let startTime |
| 16 | + |
| 17 | + const testDetails = { name: test.name, nesting: suiteStack.length, testNumber: index, skip: test.skip } |
| 18 | + const startEvent = { type: 'test:start', data: { name: test.name, nesting: suiteStack.length, type: test.type } } |
| 19 | + const pass = (duration) => ({ |
| 20 | + type: 'test:pass', |
| 21 | + data: { details: { duration_ms: duration, type: test.type }, ...testDetails } |
| 22 | + }) |
| 23 | + if (test.skip) { |
| 24 | + yield startEvent |
| 25 | + yield pass(0) |
| 26 | + } else { |
| 27 | + try { |
| 28 | + await RunSerially(suite.beforeEach) |
| 29 | + yield startEvent |
| 30 | + startTime = Date.now() |
| 31 | + |
| 32 | + if (test.type === 'suite') { |
| 33 | + suiteStack.push({ before: [], beforeEach: [], after: [], afterEach: [], tests: [], name: test.name }) |
| 34 | + try { |
| 35 | + await test.fn() |
| 36 | + for await (const event of runTestSuite({ only: options.only ? options.only.slice(1) : undefined })) { |
| 37 | + yield event |
| 38 | + } |
| 39 | + } finally { |
| 40 | + suiteStack.pop() |
| 41 | + } |
| 42 | + } else { |
| 43 | + await test.fn() |
| 44 | + } |
| 45 | + yield pass(Date.now() - startTime) |
| 46 | + } catch (e) { |
| 47 | + const error = new Error('[TEST FAILURE]', { cause: e }) |
| 48 | + yield { |
| 49 | + type: 'test:fail', |
| 50 | + data: { |
| 51 | + details: { duration_ms: startTime ? Date.now() - startTime : NaN, type: test.type, error }, |
| 52 | + ...testDetails |
| 53 | + } |
| 54 | + } |
| 55 | + throw e |
| 56 | + } finally { |
| 57 | + await RunSerially(suite.afterEach) |
| 58 | + } |
| 59 | + } |
| 60 | + } |
23 | 61 | } finally { |
24 | | - await suite.afterEach(); |
| 62 | + await RunSerially(suite.after) |
25 | 63 | } |
26 | | -}; |
| 64 | +} |
27 | 65 |
|
28 | | -async function runSuite(suite) { |
29 | | - const prefix = " ".repeat(suite.depth); |
30 | | - const tests = suite.only.length > 0 ? suite.only : suite.tests; |
31 | | - rgb.cyanln(prefix + suite.headline + " "); |
32 | | - try { |
33 | | - await suite.beforeAll(); |
34 | | - await RunSerially(tests); |
35 | | - } finally { |
36 | | - await suite.afterAll(); |
| 66 | +const suiteStack = [] |
| 67 | + |
| 68 | +function top() { |
| 69 | + if (!suiteStack.length) { |
| 70 | + suiteStack.push({ before: [], beforeEach: [], after: [], afterEach: [], tests: [], name: '' }) |
37 | 71 | } |
| 72 | + return suiteStack[suiteStack.length - 1] |
38 | 73 | } |
39 | 74 |
|
40 | | -function makeSuite(headline, only = false, fn = null) { |
41 | | - const parent = fn ? topSafe() : top(); |
42 | | - const self = { |
43 | | - depth: parent ? parent.depth + 1 : 0, |
44 | | - headline, |
45 | | - beforeAllHooks: [], |
46 | | - async beforeAll() { |
47 | | - await RunSerially(this.beforeAllHooks); |
48 | | - }, |
49 | | - beforeEachHooks: [], |
50 | | - async beforeEach() { |
51 | | - if (parent) await parent.beforeEach(); |
52 | | - await RunSerially(this.beforeEachHooks); |
53 | | - }, |
54 | | - afterAllHooks: [], |
55 | | - async afterAll() { |
56 | | - await RunSerially(this.afterAllHooks); |
57 | | - }, |
58 | | - afterEachHooks: [], |
59 | | - async afterEach() { |
60 | | - await RunSerially(this.afterEachHooks); |
61 | | - if (parent) await parent.afterEach(); |
62 | | - }, |
63 | | - tests: [], |
64 | | - only: [], |
65 | | - }; |
66 | | - if (parent && only) parent.only.push(() => runSuite(self)); |
67 | | - if (parent && !only) parent.tests.push(() => runSuite(self)); |
68 | | - suiteStack.push(self); |
69 | | - if (fn) fn(); |
70 | | - if (fn) suiteStack.pop(); |
71 | | - if (self.depth === 0) { |
72 | | - testRunPromise = new Promise((resolve) => setTimeout(resolve, 0)).then(() => { |
73 | | - suiteStack.pop(); |
74 | | - return runSuite(self); |
75 | | - }); |
| 75 | +const describe = (name, fn) => top().tests.push({ name: name, type: 'suite', fn, skip: false, only: false }) |
| 76 | +describe.skip = (name, fn) => top().tests.push({ name: name, type: 'suite', fn, skip: true, only: false }) |
| 77 | +describe.only = (name, fn) => top().tests.push({ name: name, type: 'suite', fn, skip: false, only: true }) |
| 78 | +const it = (name, fn) => top().tests.push({ name: name, type: 'test', fn, skip: false, only: false }) |
| 79 | +it.only = (name, fn) => top().tests.push({ name: name, type: 'test', fn, skip: false, only: true }) |
| 80 | +it.skip = (name, fn) => top().tests.push({ name: name, type: 'test', fn, skip: true, only: false }) |
| 81 | +const before = (fn) => top().before.push(fn) |
| 82 | +const beforeEach = (fn) => top().beforeEach.push(fn) |
| 83 | +const after = (fn) => top().after.push(fn) |
| 84 | +const afterEach = (fn) => top().afterEach.push(fn) |
| 85 | + |
| 86 | +async function* runTests(options = {}) { |
| 87 | + let index = 0 |
| 88 | + for await (const file of options.files) { |
| 89 | + const name = file |
| 90 | + const suiteDetails = { name: name, nesting: 0, testNumber: index, skip: false } |
| 91 | + suiteStack.push({ before: [], beforeEach: [], after: [], afterEach: [], tests: [], name }) |
| 92 | + let suiteStart |
| 93 | + try { |
| 94 | + require(file) |
| 95 | + yield { type: 'test:start', data: { name: name, nesting: 0, type: 'suite' } } |
| 96 | + suiteStart = Date.now() |
| 97 | + for await (let event of runTestSuite({ only: options.only })) { |
| 98 | + yield event |
| 99 | + } |
| 100 | + yield { |
| 101 | + type: 'test:pass', |
| 102 | + data: { details: { duration_ms: Date.now() - suiteStart, type: 'suite' }, ...suiteDetails } |
| 103 | + } |
| 104 | + } catch (e) { |
| 105 | + yield { |
| 106 | + type: 'test:fail', |
| 107 | + data: { |
| 108 | + details: { duration_ms: suiteStart ? Date.now() - suiteStart : NaN, type: 'suite', error: e }, |
| 109 | + ...suiteDetails |
| 110 | + } |
| 111 | + } |
| 112 | + throw e |
| 113 | + } finally { |
| 114 | + suiteStack.pop() |
| 115 | + index++ |
| 116 | + } |
76 | 117 | } |
77 | | - return self; |
78 | 118 | } |
79 | 119 |
|
80 | | -const describe = (headline, fn) => makeSuite(headline, false, fn); |
81 | | -describe.skip = () => {}; |
82 | | -describe.only = (headline, fn) => makeSuite(headline, true, fn); |
83 | | -const it = (name, fn) => topSafe().tests.push(registerTest(topSafe(), name, fn)); |
84 | | -it.only = (name, fn) => topSafe().only.push(registerTest(topSafe(), name, fn)); |
85 | | -it.skip = () => {}; |
86 | | -const before = (fn) => topSafe().beforeAllHooks.push(fn); |
87 | | -const after = (fn) => topSafe().afterAllHooks.push(fn); |
88 | | -const beforeEach = (fn) => topSafe().beforeEachHooks.push(fn); |
89 | | -const afterEach = (fn) => topSafe().afterEachHooks.push(fn); |
| 120 | +if (!suiteStack.length) { |
| 121 | + new Promise((resolve) => setTimeout(resolve, 0)).then(async () => { |
| 122 | + if (suiteStack.length) { |
| 123 | + for await (let event of runTestSuite()) { |
| 124 | + const prefix = ' '.repeat(event.data.nesting) |
| 125 | + if (event.type === 'test:start' && event.data.type === 'suite') { |
| 126 | + if (path.isAbsolute(event.data.name)) { |
| 127 | + console.log( |
| 128 | + `\x1b[36m${prefix}${path.parse(event.data.name).name} (${path.relative('./', event.data.name)})\x1b[0m` |
| 129 | + ) |
| 130 | + } else { |
| 131 | + console.log(`\x1b[36m${prefix}${event.data.name}\x1b[0m`) |
| 132 | + } |
| 133 | + } else if (event.type === 'test:pass' && event.data.details.type === 'test' && !event.data.skip) { |
| 134 | + console.log(`\x1b[32m\n${prefix}✓\x1b[0m\x1b[90m ${event.data.name}\n\x1b[0m`) |
| 135 | + } else if (event.type === 'test:fail' && event.data.details.type === 'test') { |
| 136 | + console.log(`\x1b[31m\n${prefix}✗ ${event.data.name}\n\x1b[0m`) |
| 137 | + } |
| 138 | + } |
| 139 | + process.exit(0) |
| 140 | + } |
| 141 | + }) |
| 142 | +} |
90 | 143 |
|
91 | | -module.exports = { |
92 | | - test: Object.assign(it, { |
93 | | - describe, |
94 | | - before, |
95 | | - after, |
96 | | - beforeEach, |
97 | | - afterEach, |
98 | | - }), |
99 | | - runner: { waitForTests: () => testRunPromise }, |
100 | | -}; |
| 144 | +module.exports = { test: Object.assign(it, { describe, before, beforeEach, after, afterEach }), run: runTests } |
0 commit comments