beartest-js 6.0.3 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.prettierrc CHANGED
@@ -1,5 +1,9 @@
1
1
  {
2
2
  "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"
5
9
  }
package/README.md CHANGED
@@ -22,7 +22,19 @@ The Beartest test runner uses common js to load files.
22
22
 
23
23
  ### Usage
24
24
 
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.
26
38
 
27
39
  ### Example
28
40
 
@@ -43,7 +55,7 @@ test.describe("Math Testing", () => {
43
55
 
44
56
  ### Running Tests
45
57
 
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.*`.
47
59
 
48
60
  Suggested package script:
49
61
 
package/beartest.js CHANGED
@@ -1,100 +1,144 @@
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 }
package/cli.js ADDED
@@ -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()
package/index.d.ts CHANGED
@@ -1,16 +1,92 @@
1
- type testFn = (name: string, fn: () => unknown) => void;
2
-
3
- type BearTest = testFn & {
4
- skip: testFn;
5
- only: testFn;
6
- describe: testFn & { skip: testFn; only: testFn };
7
- before(fn: () => unknown): void;
8
- after(fn: () => unknown): void;
9
- beforeEach(fn: () => unknown): void;
10
- afterEach(fn: () => unknown): void;
11
- };
12
-
13
- export declare const test: BearTest;
14
- export declare const runner: {
15
- waitForTests: Promise<void>;
16
- };
1
+ export type TestConfig = {
2
+ name: string
3
+ skip: boolean
4
+ only: boolean
5
+ type: 'suite' | 'test'
6
+ fn: () => unknown
7
+ }
8
+
9
+ export interface TestSuite {
10
+ before: (() => unknown)[]
11
+ beforeEach: (() => unknown)[]
12
+ after: (() => unknown)[]
13
+ afterEach: (() => unknown)[]
14
+ tests: TestConfig[]
15
+ name: string
16
+ }
17
+
18
+ export type TestEvent = TestStart | TestPass | TestFail
19
+
20
+ export interface TestStart {
21
+ type: 'test:start'
22
+ data: {
23
+ /** The test name. */
24
+ name: string
25
+ /** The nesting level of the test. */
26
+ nesting: number
27
+ type: 'suite' | 'test' | undefined
28
+ }
29
+ }
30
+
31
+ export interface TestPass {
32
+ type: 'test:pass'
33
+ data: {
34
+ details: {
35
+ /** The duration of the test in milliseconds. */
36
+ duration_ms: number
37
+ /** The type of the test, used to denote whether this is a suite. */
38
+ type: 'suite' | 'test' | undefined
39
+ }
40
+ /** The test name. */
41
+ name: string
42
+ /** The nesting level of the test. */
43
+ nesting: number
44
+ /** The ordinal number of the test. */
45
+ testNumber: number
46
+ /** Present if context.skip is called. */
47
+ skip: boolean
48
+ }
49
+ }
50
+
51
+ export interface TestFail {
52
+ type: 'test:fail'
53
+ data: {
54
+ /** Additional execution metadata. */
55
+ details: {
56
+ /** The duration of the test in milliseconds. */
57
+ duration_ms: number
58
+ /** An error wrapping the error thrown by the test. */
59
+ error: Error
60
+ /** The type of the test, used to denote whether this is a suite. */
61
+ type: 'suite' | 'test' | undefined
62
+ }
63
+ /** The test name. */
64
+ name: string
65
+ /** The nesting level of the test. */
66
+ nesting: number
67
+ /** The ordinal number of the test. */
68
+ testNumber: number
69
+ /** Present if context.skip is called. */
70
+ skip: boolean
71
+ }
72
+ }
73
+
74
+ export interface TestFunction {
75
+ (name: string, fn: () => Promise<any>): void
76
+ only(name: string, fn: () => Promise<any>): void
77
+ skip(name: string, fn: () => Promise<any>): void
78
+ describe: DescribeFunction
79
+ before(callback: () => Promise<any>): void
80
+ beforeEach(callback: () => Promise<any>): void
81
+ after(callback: () => Promise<any>): void
82
+ afterEach(callback: () => Promise<any>): void
83
+ }
84
+
85
+ export interface DescribeFunction {
86
+ (name: string, fn: () => unknown): void
87
+ only: (name: string, fn: () => unknown) => void
88
+ skip: (name: string, fn: () => unknown) => void
89
+ }
90
+
91
+ export const test: TestFunction
92
+ export const run: (options: { files: AsyncIterable<string> | Iterable<string> }) => AsyncGenerator<TestEvent>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beartest-js",
3
- "version": "6.0.3",
3
+ "version": "7.0.0",
4
4
  "description": "Bear Bones Testing",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,21 +8,23 @@
8
8
  },
9
9
  "types": "./index.d.ts",
10
10
  "main": "./index.js",
11
+ "exports": {
12
+ ".": {
13
+ "require": "./index.js",
14
+ "import": "./index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
11
18
  "bin": {
12
- "beartest": "testrunner.js"
19
+ "beartest": "cli.js"
13
20
  },
14
21
  "author": "Grant Colestock",
15
22
  "license": "MIT",
16
23
  "type": "commonjs",
17
- "dependencies": {
18
- "barecolor": "^1.0.1",
19
- "tiny-glob": "^0.2.8"
20
- },
21
24
  "bugs": {
22
25
  "url": "https://github.com/rubber-duck-software/beartest/issues"
23
26
  },
24
27
  "homepage": "https://github.com/rubber-duck-software/beartest#readme",
25
- "devDependencies": {},
26
28
  "scripts": {
27
29
  "test": "echo \"Error: no test specified\" && exit 1"
28
30
  }
package/testrunner.js DELETED
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env node
2
- const beartest = require("./beartest");
3
- const glob = require("tiny-glob");
4
-
5
- async function runTests() {
6
- try {
7
- const globStr = process.argv[2] || "**/*.test.*";
8
- const files = await glob(globStr, { absolute: true });
9
- for (const file of files) {
10
- require(file);
11
- await beartest.runner.waitForTests();
12
- }
13
- process.exit(0);
14
- } catch (e) {
15
- console.error(e);
16
- process.exit(1);
17
- }
18
- }
19
-
20
- runTests();