Skip to content

Commit 1866020

Browse files
v7
1 parent 3fe1ece commit 1866020

File tree

8 files changed

+273
-162
lines changed

8 files changed

+273
-162
lines changed

.prettierrc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
22
"tabWidth": 2,
3-
"useTabs": false,
4-
"printWidth": 100
3+
"printWidth": 125,
4+
"singleQuote": true,
5+
"semi": false,
6+
"trailingComma": "none",
7+
"bracketSpacing": true,
8+
"arrowParens": "always"
59
}

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,19 @@ The Beartest test runner uses common js to load files.
2222

2323
### Usage
2424

25-
_Beartest_ implements the following functions `describe`, `it`, `beforeAll`, `beforeEach`, `afterEach`, `afterAll`, `it.skip`, and `it.only`. All provided functions work in a similar way as the corresponding functions in Jest.
25+
_Beartest_ exports a `test` function that provides the following API:
26+
- `test(name, fn)` - Define a test
27+
- `test.describe(name, fn)` - Group tests into a suite
28+
- `test.before(fn)` - Run before all tests in a suite
29+
- `test.beforeEach(fn)` - Run before each test in a suite
30+
- `test.after(fn)` - Run after all tests in a suite
31+
- `test.afterEach(fn)` - Run after each test in a suite
32+
- `test.skip(name, fn)` - Skip a test
33+
- `test.only(name, fn)` - Run only this test
34+
- `test.describe.skip(name, fn)` - Skip a suite
35+
- `test.describe.only(name, fn)` - Run only this suite
36+
37+
All provided functions work in a similar way as the corresponding functions in Jest.
2638

2739
### Example
2840

@@ -43,7 +55,7 @@ test.describe("Math Testing", () => {
4355

4456
### Running Tests
4557

46-
Additionally, a very basic test runner is included. This test runner accepts a glob pattern as a command line argument. The test runner can be invoked with `yarn beartest "glob-pattern"`. By default, it will look for `**/*.test.js`.
58+
Additionally, a very basic test runner is included. This test runner accepts a glob pattern as a command line argument. The test runner can be invoked with `yarn beartest "glob-pattern"`. By default, it will look for `**/*.test.*`.
4759

4860
Suggested package script:
4961

beartest.js

Lines changed: 130 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,144 @@
1-
const rgb = require("barecolor");
2-
1+
const path = require('node:path')
32
async function RunSerially(fnArray) {
4-
for (const fn of fnArray) {
5-
await fn();
6-
}
3+
for (const fn of fnArray) await fn()
74
}
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(""));
125

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+
)
1511
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+
}
2361
} finally {
24-
await suite.afterEach();
62+
await RunSerially(suite.after)
2563
}
26-
};
64+
}
2765

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: '' })
3771
}
72+
return suiteStack[suiteStack.length - 1]
3873
}
3974

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+
}
76117
}
77-
return self;
78118
}
79119

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+
}
90143

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 }

cli.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env node
2+
const { run } = require('./beartest')
3+
const fs = require('node:fs')
4+
5+
async function cli() {
6+
const globStr = process.argv[2] || '**/*.test.*'
7+
8+
const files = await Array.fromAsync(fs.promises.glob(globStr))
9+
10+
for await (let event of run({ files: files })) {
11+
const prefix = ' '.repeat(event.data.nesting)
12+
if (event.type === 'test:start' && event.data.type === 'suite') {
13+
process.stdout.write(`\x1b[36m${prefix}${event.data.name} \n\x1b[0m`)
14+
} else if (event.type === 'test:pass' && event.data.details.type === 'test' && !event.data.skip) {
15+
process.stdout.write(`\x1b[32m${prefix}✓\x1b[0m\x1b[90m ${event.data.name}\n\x1b[0m`)
16+
} else if (event.type === 'test:fail' && event.data.details.type === 'test') {
17+
process.stdout.write(`\x1b[31m\n${prefix}${event.data.name} \n\n\x1b[0m`)
18+
}
19+
}
20+
}
21+
22+
cli()

0 commit comments

Comments
 (0)