├── .editorconfig ├── .eslintrc.json ├── .github ├── funding.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── TestDirector.mjs ├── TestDirector.test.mjs ├── changelog.md ├── jsconfig.json ├── license.md ├── package.json ├── readme.md ├── reportError.mjs ├── test.mjs └── test ├── fixtures ├── awaits.mjs ├── fails.mjs ├── nested.mjs ├── output.mjs └── passes.mjs ├── sleep.mjs └── snapshots └── TestDirector ├── test-fails-stderr.ans ├── test-fails-stdout.ans ├── test-nested-stderr.ans ├── test-nested-stdout.ans ├── test-outputs-stdout.ans ├── test-passes-stdout.ans └── test-sequence-stdout.ans /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | max_line_length = 80 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended"], 3 | "env": { 4 | "es2022": true, 5 | "node": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": "latest" 9 | }, 10 | "plugins": ["simple-import-sort"], 11 | "rules": { 12 | "simple-import-sort/imports": "error", 13 | "simple-import-sort/exports": "error" 14 | }, 15 | "overrides": [ 16 | { 17 | "files": ["*.mjs"], 18 | "parserOptions": { 19 | "sourceType": "module" 20 | }, 21 | "globals": { 22 | "__dirname": "off", 23 | "__filename": "off", 24 | "exports": "off", 25 | "module": "off", 26 | "require": "off" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: jaydenseric 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Test with Node.js v${{ matrix.node }} and ${{ matrix.os }} 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest] 10 | node: ["16", "18", "19"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Setup Node.js v${{ matrix.node }} 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: ${{ matrix.node }} 17 | - name: npm install and test 18 | run: npm install-test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | /test/snapshots 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "never" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.disableAutomaticTypeAcquisition": true, 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "typescript.tsdk": "node_modules/typescript/lib" 5 | } 6 | -------------------------------------------------------------------------------- /TestDirector.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { bold, green, red } from "kleur/colors"; 4 | 5 | import reportError from "./reportError.mjs"; 6 | 7 | /** An ultra lightweight unit test director for Node.js. */ 8 | export default class TestDirector { 9 | constructor() { 10 | /** 11 | * A map of test functions that have been added, keyed by their test names. 12 | * @type {Map} 13 | */ 14 | this.tests = new Map(); 15 | } 16 | 17 | /** 18 | * Adds a test. 19 | * @param {string} name Unique test name. 20 | * @param {Function} test Test to run; may return a `Promise`. 21 | * @example 22 | * A sync test: 23 | * 24 | * ```js 25 | * import { equal } from "node:assert"; 26 | * import TestDirector from "test-director"; 27 | * 28 | * const tests = new TestDirector(); 29 | * 30 | * tests.add("JavaScript addition.", () => { 31 | * equal(1 + 1, 2); 32 | * }); 33 | * 34 | * tests.run(); 35 | * ``` 36 | * @example 37 | * An async test: 38 | * 39 | * ```js 40 | * import { ok } from "node:assert"; 41 | * import TestDirector from "test-director"; 42 | * 43 | * const tests = new TestDirector(); 44 | * 45 | * tests.add("GitHub is up.", async () => { 46 | * const response = await fetch("https://github.com"); 47 | * ok(response.ok); 48 | * }); 49 | * 50 | * tests.run(); 51 | * ``` 52 | */ 53 | add(name, test) { 54 | if (typeof name !== "string") 55 | throw new TypeError("Argument 1 `name` must be a string."); 56 | 57 | if (this.tests.has(name)) 58 | throw new Error(`A test called \`${name}\` has already been added.`); 59 | 60 | if (typeof test !== "function") 61 | throw new TypeError("Argument 2 `test` must be a function."); 62 | 63 | this.tests.set(name, test); 64 | } 65 | 66 | /** 67 | * Runs the tests one after another, in the order they were added. 68 | * @param {boolean} [throwOnFailure] After tests run, throw an error if some 69 | * failed. Defaults to `false`. 70 | * @returns {Promise} Resolves once tests have run. 71 | * @example 72 | * Nested tests: 73 | * 74 | * ```js 75 | * import TestDirector from "test-director"; 76 | * 77 | * const tests = new TestDirector(); 78 | * 79 | * tests.add("Test A.", async () => { 80 | * const tests = new TestDirector(); 81 | * 82 | * tests.add("Test B.", () => { 83 | * // … 84 | * }); 85 | * 86 | * tests.add("Test C.", () => { 87 | * // … 88 | * }); 89 | * 90 | * await tests.run(true); 91 | * }); 92 | * 93 | * tests.add("Test D.", () => { 94 | * // … 95 | * }); 96 | * 97 | * tests.run(); 98 | * ``` 99 | */ 100 | async run(throwOnFailure = false) { 101 | let passCount = 0; 102 | 103 | for (const [name, test] of this.tests) { 104 | console.group(`\nTest: ${bold(name)}`); 105 | 106 | try { 107 | await test(); 108 | passCount++; 109 | } catch (error) { 110 | reportError(error); 111 | } finally { 112 | console.groupEnd(); 113 | } 114 | } 115 | 116 | const summary = `${passCount}/${this.tests.size} tests passed.`; 117 | 118 | if (passCount < this.tests.size) { 119 | const message = bold(red(summary)); 120 | 121 | if (throwOnFailure) throw new Error(message); 122 | 123 | console.error(`\n${message}\n`); 124 | 125 | process.exitCode = 1; 126 | } else console.info(`\n${bold(green(summary))}\n`); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /TestDirector.test.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { strictEqual, throws } from "node:assert"; 4 | import { spawnSync } from "node:child_process"; 5 | import { fileURLToPath } from "node:url"; 6 | 7 | import replaceStackTraces from "replace-stack-traces"; 8 | import assertSnapshot from "snapshot-assertion"; 9 | 10 | import TestDirector from "./TestDirector.mjs"; 11 | 12 | /** 13 | * Adds `TestDirector` tests. 14 | * @param {import("test-director").default} tests Test director. 15 | */ 16 | export default function test_TestDirector(tests) { 17 | tests.add("`TestDirector` with an invalid test name type.", () => { 18 | const tests = new TestDirector(); 19 | 20 | throws(() => { 21 | tests.add( 22 | // @ts-ignore Testing invalid. 23 | true, 24 | () => {} 25 | ); 26 | }, new TypeError("Argument 1 `name` must be a string.")); 27 | }); 28 | 29 | tests.add("`TestDirector` adding two tests with the same name.", () => { 30 | const tests = new TestDirector(); 31 | 32 | tests.add("a", () => {}); 33 | 34 | throws(() => { 35 | tests.add("a", () => {}); 36 | }, new Error("A test called `a` has already been added.")); 37 | }); 38 | 39 | tests.add("`TestDirector` with an invalid test function type.", () => { 40 | const tests = new TestDirector(); 41 | 42 | throws(() => { 43 | tests.add( 44 | "a", 45 | // @ts-ignore Testing invalid. 46 | "" 47 | ); 48 | }, new TypeError("Argument 2 `test` must be a function.")); 49 | }); 50 | 51 | tests.add("`TestDirector` test passes.", async () => { 52 | const { stdout, stderr, status, error } = spawnSync( 53 | "node", 54 | [fileURLToPath(new URL("./test/fixtures/passes.mjs", import.meta.url))], 55 | { 56 | env: { 57 | ...process.env, 58 | FORCE_COLOR: "1", 59 | }, 60 | } 61 | ); 62 | 63 | if (error) throw error; 64 | 65 | await assertSnapshot( 66 | stdout.toString(), 67 | new URL( 68 | "./test/snapshots/TestDirector/test-passes-stdout.ans", 69 | import.meta.url 70 | ) 71 | ); 72 | 73 | strictEqual(stderr.toString(), ""); 74 | strictEqual(status, 0); 75 | }); 76 | 77 | tests.add("`TestDirector` test with console output.", async () => { 78 | const { stdout, stderr, status, error } = spawnSync( 79 | "node", 80 | [fileURLToPath(new URL("./test/fixtures/output.mjs", import.meta.url))], 81 | { 82 | env: { 83 | ...process.env, 84 | FORCE_COLOR: "1", 85 | }, 86 | } 87 | ); 88 | 89 | if (error) throw error; 90 | 91 | await assertSnapshot( 92 | stdout.toString(), 93 | new URL( 94 | "./test/snapshots/TestDirector/test-outputs-stdout.ans", 95 | import.meta.url 96 | ) 97 | ); 98 | 99 | strictEqual(stderr.toString(), ""); 100 | strictEqual(status, 0); 101 | }); 102 | 103 | tests.add("`TestDirector` awaits tests in sequence.", async () => { 104 | const { stdout, stderr, status, error } = spawnSync( 105 | "node", 106 | [fileURLToPath(new URL("./test/fixtures/awaits.mjs", import.meta.url))], 107 | { 108 | env: { 109 | ...process.env, 110 | FORCE_COLOR: "1", 111 | }, 112 | } 113 | ); 114 | 115 | if (error) throw error; 116 | 117 | await assertSnapshot( 118 | stdout.toString(), 119 | new URL( 120 | "./test/snapshots/TestDirector/test-sequence-stdout.ans", 121 | import.meta.url 122 | ) 123 | ); 124 | 125 | strictEqual(stderr.toString(), ""); 126 | strictEqual(status, 0); 127 | }); 128 | 129 | tests.add("`TestDirector` test fails.", async () => { 130 | const { stdout, stderr, status, error } = spawnSync( 131 | "node", 132 | [fileURLToPath(new URL("./test/fixtures/fails.mjs", import.meta.url))], 133 | { 134 | env: { 135 | ...process.env, 136 | FORCE_COLOR: "1", 137 | }, 138 | } 139 | ); 140 | 141 | if (error) throw error; 142 | 143 | await assertSnapshot( 144 | stdout.toString(), 145 | new URL( 146 | "./test/snapshots/TestDirector/test-fails-stdout.ans", 147 | import.meta.url 148 | ) 149 | ); 150 | 151 | await assertSnapshot( 152 | replaceStackTraces(stderr.toString()), 153 | new URL( 154 | "./test/snapshots/TestDirector/test-fails-stderr.ans", 155 | import.meta.url 156 | ) 157 | ); 158 | 159 | strictEqual(status, 1); 160 | }); 161 | 162 | tests.add("`TestDirector` nested tests.", async () => { 163 | const { stdout, stderr, status, error } = spawnSync( 164 | "node", 165 | [fileURLToPath(new URL("./test/fixtures/nested.mjs", import.meta.url))], 166 | { 167 | env: { 168 | ...process.env, 169 | FORCE_COLOR: "1", 170 | }, 171 | } 172 | ); 173 | 174 | if (error) throw error; 175 | 176 | await assertSnapshot( 177 | stdout.toString(), 178 | new URL( 179 | "./test/snapshots/TestDirector/test-nested-stdout.ans", 180 | import.meta.url 181 | ) 182 | ); 183 | 184 | await assertSnapshot( 185 | replaceStackTraces(stderr.toString()), 186 | new URL( 187 | "./test/snapshots/TestDirector/test-nested-stderr.ans", 188 | import.meta.url 189 | ) 190 | ); 191 | 192 | strictEqual(status, 1); 193 | }); 194 | } 195 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # test-director changelog 2 | 3 | ## 11.0.0 4 | 5 | ### Major 6 | 7 | - Updated Node.js support to `^16.9.0 || >= 18.0.0`. 8 | - Tests now report a thrown `AggregateError` instance property `errors`. 9 | - Tests now report a thrown `Error` instance property `cause`. 10 | 11 | ### Patch 12 | 13 | - Updated dev dependencies. 14 | - Use [`test-director`](https://npm.im/test-director) as a dev dependency for tests. 15 | - Updated GitHub Actions CI config: 16 | - Run tests with Node.js v16, v18, v19. 17 | - Improved the installation instructions in the readme. 18 | - Remove [`node-fetch`](https://npm.im/node-fetch) from code examples. Modern Node.js supports the `fetch` global. 19 | 20 | ## 10.0.0 21 | 22 | ### Major 23 | 24 | - Removed the package `main` field. 25 | - Use the `node:` URL scheme for Node.js builtin module imports. 26 | 27 | ### Patch 28 | 29 | - Updated dev dependencies. 30 | 31 | ## 9.0.0 32 | 33 | ### Major 34 | 35 | - Updated Node.js support to `^14.17.0 || ^16.0.0 || >= 18.0.0`. 36 | 37 | ### Patch 38 | 39 | - Updated dependencies. 40 | - Removed the [`@types/stack-utils`](https://npm.im/@types/stack-utils) and [`stack-utils`](https://npm.im/stack-utils) dependencies; error details are now output in a much simpler and more reliable way. 41 | - Added a new [`replace-stack-traces`](https://npm.im/replace-stack-traces) dev dependency for replacing error stack traces in test snapshots, and removed the test helper function `simulatePublishedTraces`. 42 | - Updated `jsconfig.json`: 43 | - Set `compilerOptions.maxNodeModuleJsDepth` to `10`. 44 | - Set `compilerOptions.module` to `nodenext`. 45 | - Updated ESLint config. 46 | - Updated GitHub Actions CI config: 47 | - Run tests with Node.js v14, v16, v18. 48 | - Updated `actions/checkout` to v3. 49 | - Updated `actions/setup-node` to v3. 50 | - Removed redundant JSDoc `@ignore` tags. 51 | - Revamped the readme: 52 | - Removed the badges. 53 | - Added information about TypeScript config and [optimal JavaScript module design](https://jaydenseric.com/blog/optimal-javascript-module-design). 54 | 55 | ## 8.0.2 56 | 57 | ### Patch 58 | 59 | - Updated dev dependencies. 60 | - Simplified dev dependencies and config for ESLint. 61 | - Removed the [`jsdoc-md`](https://npm.im/jsdoc-md) dev dependency and the related package scripts, replacing the readme “API” section with manually written “Examples” and “Exports” sections. 62 | - Updated `jsconfig.json` to disable TypeScript automatic type acquisition for the project. 63 | - Moved the test index module. 64 | - Added a `license.md` MIT License file. 65 | 66 | ## 8.0.1 67 | 68 | ### Patch 69 | 70 | - Moved [`@types/node`](https://npm.im/@types/node) from package `devDependencies` to `dependencies`, using `*` for the version. 71 | - Moved [`@types/stack-utils`](https://npm.im/@types/stack-utils) from package `devDependencies` to `dependencies`. 72 | 73 | ## 8.0.0 74 | 75 | ### Major 76 | 77 | - Updated Node.js support to `^12.22.0 || ^14.17.0 || >= 16.0.0`. 78 | - Updated dev dependencies, some of which require newer Node.js versions than previously supported. 79 | - Removed `./package` from the package `exports` field; the full `package.json` filename must be used in a `require` path. 80 | - Renamed `index.mjs` to `TestDirector.mjs` and added it to the package `exports` field. 81 | - Implemented TypeScript types via JSDoc comments. 82 | 83 | ### Patch 84 | 85 | - Simplified package scripts. 86 | - Check TypeScript types via a new package `types` script. 87 | - Stopped using the [`kleur`](https://npm.im/kleur) chaining API. 88 | - Configured Prettier option `singleQuote` to the default, `false`. 89 | - Fixed a JSDoc example code bug. 90 | - Documentation tweaks. 91 | 92 | ## 7.0.0 93 | 94 | ### Major 95 | 96 | - Updated Node.js support to `^12.20.0 || ^14.13.1 || >= 16.0.0`. 97 | - Updated dev dependencies, some of which require newer Node.js versions than previously supported. 98 | 99 | ### Patch 100 | 101 | - Updated dependencies. 102 | - Replaced the package `prepare` script with a `jsdoc` script. 103 | - Added a package `test:jsdoc` script that checks the readme API docs are up to date with the source JSDoc. 104 | - Also run GitHub Actions CI with Node.js v17, and drop v15. 105 | - Prevent outputting the Node.js internal `async Promise.all (index 0)` error stack frame generated by recent Node.js versions that [`stack-utils`](https://npm.im/stack-utils) fails to clean, (see [tapjs/stack-utils#63](https://github.com/tapjs/stack-utils/issues/63)). 106 | - Readme tweaks. 107 | 108 | ## 6.0.0 109 | 110 | ### Major 111 | 112 | - Updated Node.js support to `^12.20 || >= 14.13`. 113 | - The API is now ESM in `.mjs` files instead of CJS in `.js` files, [accessible via `import` but not `require`](https://nodejs.org/dist/latest/docs/api/esm.html#esm_require). 114 | - The `TestDirector` class is now only accessible via a default `import` from the main index. 115 | - The `TesDirector` instance method `add` now throws a `TypeError` if argument 1 `name` is not a string. 116 | 117 | ### Patch 118 | 119 | - Updated dependencies. 120 | - Stop using [`hard-rejection`](https://npm.im/hard-rejection) to detect unhandled `Promise` rejections in tests, as Node.js v15+ does this natively. 121 | - Updated GitHub Actions CI config: 122 | - Also run tests with Node.js v16. 123 | - Updated `actions/checkout` to v2. 124 | - Updated `actions/setup-node` to v2. 125 | - Don’t specify the `CI` environment variable as it’s set by default. 126 | - Use the regex flag `u`. 127 | - The `TesDirector` instance method `add` now throws a more specific `TypeError` if argument 2 `test` is not a function. 128 | - Stop snapshot testing major Node.js versions separately, as they all produce the same results now. 129 | - Changed the [`snapshot-assertion`](https://npm.im/snapshot-assertion`) link in the readme from GitHub to npm. 130 | - Recommend [`coverage-node`](https://npm.im/coverage-node) in the readme. 131 | - Prettier formatting fix for a code example. 132 | - Whitespace formatting tweaks. 133 | - Linked [Node.js](https://nodejs.org) in the readme. 134 | 135 | ## 5.0.0 136 | 137 | ### Major 138 | 139 | - Updated supported Node.js versions to `^10.17.0 || ^12.0.0 || >= 13.7.0`. 140 | - Updated dependencies, some of which require newer Node.js versions than were previously supported. 141 | - The updated [`kleur`](https://npm.im/kleur) dependency causes subtle differences in which environments get colored console output. 142 | - Published files have been reorganized, so previously supported deep imports will need to be rewritten according to the newly documented paths. 143 | - Removed the package `module` field. 144 | - The summary message when tests fail now outputs using `stderr` via `console.error` instead of using `stdout` via `console.info`. 145 | 146 | ### Patch 147 | 148 | - Removed Node.js v13 and added v15 to the versions tested in GitHub Actions CI. 149 | - Simplified the GitHub Actions CI config with the [`npm install-test`](https://docs.npmjs.com/cli/v7/commands/npm-install-test) command. 150 | - Updated the EditorConfig. 151 | - Use destructuring for `require` of the Node.js `path` API in tests. 152 | - Use the `FORCE_COLOR` environment variable in tests to ensure output is colorized. 153 | - Use the `.ans` file extension for snapshot text files containing ANSI colorization. 154 | - Removed `npm-debug.log` from the `.gitignore` file as npm [v4.2.0](https://github.com/npm/npm/releases/tag/v4.2.0)+ doesn’t create it in the current working directory. 155 | - When tests fail and the `throwOnFailure` option is used, don’t set the process exit code to `1`. 156 | - Tweaked the order of ANSI escape codes in messages so modifiers come before colors. 157 | 158 | ## 4.0.1 159 | 160 | ### Patch 161 | 162 | - Updated dependencies. 163 | - Updated Node.js support to `10 - 12 || >= 13.7` to reflect the package `exports` related breaking changes. 164 | - Updated the package `exports` field to allow requiring `package.json` and specific deep imports. 165 | - Also run GitHub Actions with Node.js v14. 166 | - Use [`snapshot-assertion`](https://npm.im/snapshot-assertion) for snapshot tests. 167 | - Mention [`snapshot-assertion`](https://npm.im/snapshot-assertion) in the readme. 168 | - Improved the package `prepare:prettier` and `test:prettier` scripts. 169 | - Configured Prettier option `semi` to the default, `true`. 170 | 171 | ## 4.0.0 172 | 173 | ### Major 174 | 175 | - Added a [package `exports` field](https://nodejs.org/api/esm.html#esm_package_exports) to support native ESM in Node.js. 176 | - Added package `sideEffects` and `module` fields for bundlers such as webpack. 177 | - Published files have been reorganized, so undocumented deep imports may no longer work. 178 | 179 | ### Patch 180 | 181 | - Updated dependencies. 182 | - Lint fixes for [`prettier`](https://npm.im/prettier) v2. 183 | - Added `esm` and `mjs` to the package `tags` field. 184 | - Ensure GitHub Actions run on pull request. 185 | - Moved the `simulatePublishedTraces` test helper into its own file. 186 | - Destructure `assert` imports in tests. 187 | - Use file extensions in require paths. 188 | - Tidied the position of a JSDoc comment. 189 | 190 | ## 3.0.1 191 | 192 | ### Patch 193 | 194 | - Updated dev dependencies. 195 | - Added a new [`hard-rejection`](https://npm.im/hard-rejection) dev dependency to ensure unhandled rejections in tests exit the process with an error. 196 | - Don’t attempt to display an error stack if it’s missing, empty, or the same as the error message. 197 | - Improved code examples. 198 | 199 | ## 3.0.0 200 | 201 | ### Major 202 | 203 | - Updated Node.js support from v8.10+ to v10+. 204 | - Updated the [`stack-utils`](https://npm.im/stack-utils) dependency to v2. 205 | - Use [`coverage-node`](https://npm.im/coverage-node) for test code coverage. 206 | 207 | ### Minor 208 | 209 | - Support tests throwing unusual error types, such as primitives. 210 | - [`test-director`](https://npm.im/test-director) is now excluded from error traces. 211 | 212 | ### Patch 213 | 214 | - Updated dev dependencies. 215 | - Changed the order of console color codes to color, then modifier. 216 | - Removed the extra newline that trails error stacks. 217 | - Implemented better tests using JS, replacing the shell scripts. 218 | - Updated code examples. 219 | - Added a readme “Support” section. 220 | 221 | ## 2.0.0 222 | 223 | ### Major 224 | 225 | - Updated Node.js support from v8.5+ to v8.10+. 226 | - Replaced the [`chalk`](https://npm.im/chalk) dependency with [`kleur`](https://npm.im/kleur), which has a much smaller install size and outputs cleaner code. Its environment color support detection may behave differently. 227 | 228 | ### Minor 229 | 230 | - Setup [GitHub Sponsors funding](https://github.com/sponsors/jaydenseric): 231 | - Added `.github/funding.yml` to display a sponsor button in GitHub. 232 | - Added a `package.json` `funding` field to enable npm CLI funding features. 233 | 234 | ### Patch 235 | 236 | - Updated dev dependencies. 237 | - Removed the now redundant [`eslint-plugin-import-order-alphabetical`](https://npm.im/eslint-plugin-import-order-alphabetical) dev dependency. 238 | - Stop using [`husky`](https://npm.im/husky) and [`lint-staged`](https://npm.im/lint-staged). 239 | - Use strict mode for scripts. 240 | - Test Node.js v13 in CI GitHub Actions. 241 | - Corrected an example caption. 242 | 243 | ## 1.0.0 244 | 245 | Initial release. 246 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "maxNodeModuleJsDepth": 10, 4 | "module": "nodenext", 5 | "noEmit": true, 6 | "strict": true 7 | }, 8 | "typeAcquisition": { 9 | "enable": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright Jayden Seric 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-director", 3 | "version": "11.0.0", 4 | "description": "An ultra lightweight unit test director for Node.js.", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Jayden Seric", 8 | "email": "me@jaydenseric.com", 9 | "url": "https://jaydenseric.com" 10 | }, 11 | "repository": "github:jaydenseric/test-director", 12 | "homepage": "https://github.com/jaydenseric/test-director#readme", 13 | "bugs": "https://github.com/jaydenseric/test-director/issues", 14 | "funding": "https://github.com/sponsors/jaydenseric", 15 | "keywords": [ 16 | "test", 17 | "director", 18 | "esm", 19 | "mjs" 20 | ], 21 | "files": [ 22 | "reportError.mjs", 23 | "TestDirector.mjs" 24 | ], 25 | "sideEffects": false, 26 | "exports": { 27 | ".": "./TestDirector.mjs", 28 | "./package.json": "./package.json", 29 | "./TestDirector.mjs": "./TestDirector.mjs" 30 | }, 31 | "engines": { 32 | "node": "^16.9.0 || >= 18.0.0" 33 | }, 34 | "dependencies": { 35 | "@types/node": "*", 36 | "kleur": "^4.1.5" 37 | }, 38 | "devDependencies": { 39 | "coverage-node": "^8.0.0", 40 | "eslint": "^8.35.0", 41 | "eslint-plugin-simple-import-sort": "^10.0.0", 42 | "prettier": "^2.8.4", 43 | "replace-stack-traces": "^2.0.0", 44 | "snapshot-assertion": "^5.0.0", 45 | "test-director": "^10.0.0", 46 | "typescript": "^4.9.5" 47 | }, 48 | "scripts": { 49 | "eslint": "eslint .", 50 | "prettier": "prettier -c .", 51 | "types": "tsc -p jsconfig.json", 52 | "tests": "coverage-node test.mjs", 53 | "test": "npm run eslint && npm run prettier && npm run types && npm run tests", 54 | "prepublishOnly": "npm test" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # test-director 2 | 3 | An ultra lightweight unit test director for Node.js. 4 | 5 | Works well with any assertion library that throws errors, such as the [Node.js `assert` API](https://nodejs.org/api/assert.html) and [`snapshot-assertion`](https://npm.im/snapshot-assertion). 6 | 7 | Use [`coverage-node`](https://npm.im/coverage-node) to run your test script and report code coverage. 8 | 9 | ## Installation 10 | 11 | To install [`test-director`](https://npm.im/test-director) with [npm](https://npmjs.com/get-npm), run: 12 | 13 | ```sh 14 | npm install test-director --save-dev 15 | ``` 16 | 17 | Then, import and use the class [`TestDirector`](./TestDirector.mjs). 18 | 19 | ## Examples 20 | 21 | A sync test: 22 | 23 | ```js 24 | import { equal } from "node:assert"; 25 | import TestDirector from "test-director"; 26 | 27 | const tests = new TestDirector(); 28 | 29 | tests.add("JavaScript addition.", () => { 30 | equal(1 + 1, 2); 31 | }); 32 | 33 | tests.run(); 34 | ``` 35 | 36 | An async test: 37 | 38 | ```js 39 | import { ok } from "node:assert"; 40 | import TestDirector from "test-director"; 41 | 42 | const tests = new TestDirector(); 43 | 44 | tests.add("GitHub is up.", async () => { 45 | const response = await fetch("https://github.com"); 46 | ok(response.ok); 47 | }); 48 | 49 | tests.run(); 50 | ``` 51 | 52 | Nested tests: 53 | 54 | ```js 55 | import TestDirector from "test-director"; 56 | 57 | const tests = new TestDirector(); 58 | 59 | tests.add("Test A.", async () => { 60 | const tests = new TestDirector(); 61 | 62 | tests.add("Test B.", () => { 63 | // … 64 | }); 65 | 66 | tests.add("Test C.", () => { 67 | // … 68 | }); 69 | 70 | await tests.run(true); 71 | }); 72 | 73 | tests.add("Test D.", () => { 74 | // … 75 | }); 76 | 77 | tests.run(); 78 | ``` 79 | 80 | ## Requirements 81 | 82 | Supported runtime environments: 83 | 84 | - [Node.js](https://nodejs.org) versions `^16.9.0 || >= 18.0.0`. 85 | 86 | Projects must configure [TypeScript](https://typescriptlang.org) to use types from the ECMAScript modules that have a `// @ts-check` comment: 87 | 88 | - [`compilerOptions.allowJs`](https://typescriptlang.org/tsconfig#allowJs) should be `true`. 89 | - [`compilerOptions.maxNodeModuleJsDepth`](https://typescriptlang.org/tsconfig#maxNodeModuleJsDepth) should be reasonably large, e.g. `10`. 90 | - [`compilerOptions.module`](https://typescriptlang.org/tsconfig#module) should be `"node16"` or `"nodenext"`. 91 | 92 | ## Exports 93 | 94 | The [npm](https://npmjs.com) package [`test-director`](https://npm.im/test-director) features [optimal JavaScript module design](https://jaydenseric.com/blog/optimal-javascript-module-design). These ECMAScript modules are exported via the [`package.json`](./package.json) field [`exports`](https://nodejs.org/api/packages.html#exports): 95 | 96 | - [`TestDirector.mjs`](./TestDirector.mjs) 97 | -------------------------------------------------------------------------------- /reportError.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { inspect } from "node:util"; 4 | 5 | import { red } from "kleur/colors"; 6 | 7 | /** 8 | * Reports an error to the console. 9 | * @param {unknown} error Error to report. 10 | */ 11 | export default function reportError(error) { 12 | console.error( 13 | `\n${red( 14 | error instanceof Error && error.stack ? error.stack : inspect(error) 15 | )}` 16 | ); 17 | 18 | if (error instanceof AggregateError && error.errors.length) { 19 | console.group(`\n${red("Aggregate errors:")}`); 20 | 21 | for (const aggregateError of /** @type {Array} **/ (error.errors)) 22 | reportError(aggregateError); 23 | 24 | console.groupEnd(); 25 | } 26 | 27 | if (error instanceof Error && error.cause) { 28 | console.group(`\n${red("Cause:")}`); 29 | 30 | reportError(error.cause); 31 | 32 | console.groupEnd(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import TestDirector from "test-director"; 4 | 5 | import test_TestDirector from "./TestDirector.test.mjs"; 6 | 7 | const tests = new TestDirector(); 8 | 9 | test_TestDirector(tests); 10 | 11 | tests.run(); 12 | -------------------------------------------------------------------------------- /test/fixtures/awaits.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import TestDirector from "../../TestDirector.mjs"; 4 | import sleep from "../sleep.mjs"; 5 | 6 | const tests = new TestDirector(); 7 | 8 | tests.add("a", async () => { 9 | await sleep(50); 10 | console.info("Message A."); 11 | }); 12 | 13 | tests.add("b", () => { 14 | console.info("Message B."); 15 | }); 16 | 17 | tests.run(); 18 | -------------------------------------------------------------------------------- /test/fixtures/fails.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { strictEqual } from "node:assert"; 4 | 5 | import TestDirector from "../../TestDirector.mjs"; 6 | 7 | const tests = new TestDirector(); 8 | 9 | tests.add("a", () => { 10 | throw new Error("Message."); 11 | }); 12 | 13 | tests.add("b", () => { 14 | // Some errors have a stack the same as the message. 15 | const message = "Message."; 16 | const error = new Error(message); 17 | error.stack = message; 18 | throw error; 19 | }); 20 | 21 | tests.add("c", () => { 22 | // Some errors don’t have a stack. 23 | const error = new Error("Message."); 24 | delete error.stack; 25 | throw error; 26 | }); 27 | 28 | tests.add("d", () => { 29 | strictEqual(0, 1); 30 | }); 31 | 32 | tests.add("e", () => { 33 | strictEqual(0, 1, "Message."); 34 | }); 35 | 36 | tests.add("f", () => { 37 | throw { message: "Message." }; 38 | }); 39 | 40 | tests.add("g", () => { 41 | throw {}; 42 | }); 43 | 44 | tests.add("h", () => { 45 | throw "Message."; 46 | }); 47 | 48 | tests.add("i", () => { 49 | throw null; 50 | }); 51 | 52 | tests.add("j", () => { 53 | throw new Error("Message C.", { 54 | cause: new Error("Message B.", { 55 | cause: new Error("Message A."), 56 | }), 57 | }); 58 | }); 59 | 60 | tests.add("k", () => { 61 | throw new Error("Message B.", { 62 | cause: new AggregateError([], "Message A."), 63 | }); 64 | }); 65 | 66 | tests.add("l", () => { 67 | throw new AggregateError([], "Message A."); 68 | }); 69 | 70 | tests.add("m", () => { 71 | throw new AggregateError( 72 | [ 73 | new Error("Message A."), 74 | new Error("Message C.", { 75 | cause: new Error("Message B."), 76 | }), 77 | new AggregateError( 78 | [ 79 | new Error("Message D."), 80 | new Error("Message F.", { 81 | cause: new Error("Message E."), 82 | }), 83 | ], 84 | "Message G." 85 | ), 86 | ], 87 | "Message H." 88 | ); 89 | }); 90 | 91 | tests.run(); 92 | -------------------------------------------------------------------------------- /test/fixtures/nested.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import TestDirector from "../../TestDirector.mjs"; 4 | 5 | const tests = new TestDirector(); 6 | 7 | tests.add("a", async () => { 8 | const tests = new TestDirector(); 9 | 10 | tests.add("b", () => { 11 | throw new Error("Message."); 12 | }); 13 | 14 | await tests.run(true); 15 | }); 16 | 17 | tests.run(); 18 | -------------------------------------------------------------------------------- /test/fixtures/output.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import TestDirector from "../../TestDirector.mjs"; 4 | 5 | const tests = new TestDirector(); 6 | 7 | tests.add("a", () => { 8 | console.info("Message."); 9 | }); 10 | 11 | tests.run(); 12 | -------------------------------------------------------------------------------- /test/fixtures/passes.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import TestDirector from "../../TestDirector.mjs"; 4 | 5 | const tests = new TestDirector(); 6 | 7 | tests.add("a", () => {}); 8 | 9 | tests.run(); 10 | -------------------------------------------------------------------------------- /test/sleep.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Sleeps the process for a specified duration. 5 | * @param {number} ms Duration in milliseconds. 6 | * @returns {Promise} Resolves once the duration is up. 7 | */ 8 | export default function sleep(ms) { 9 | return new Promise((resolve) => setTimeout(resolve, ms)); 10 | } 11 | -------------------------------------------------------------------------------- /test/snapshots/TestDirector/test-fails-stderr.ans: -------------------------------------------------------------------------------- 1 | 2 | Error: Message. 3 |  4 | 5 | Message. 6 | 7 | [Error: Message.] 8 | 9 | AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 10 | 11 | 0 !== 1 12 | 13 |  14 | 15 | AssertionError [ERR_ASSERTION]: Message. 16 |  17 | 18 | { message: 'Message.' } 19 | 20 | {} 21 | 22 | 'Message.' 23 | 24 | null 25 | 26 | Error: Message C. 27 |  28 | 29 | Error: Message B. 30 |  31 | 32 | Error: Message A. 33 |  34 | 35 | Error: Message B. 36 |  37 | 38 | AggregateError: Message A. 39 |  40 | 41 | AggregateError: Message A. 42 |  43 | 44 | AggregateError: Message H. 45 |  46 | 47 | Error: Message A. 48 |  49 | 50 | Error: Message C. 51 |  52 | 53 | Error: Message B. 54 |  55 | 56 | AggregateError: Message G. 57 |  58 | 59 | Error: Message D. 60 |  61 | 62 | Error: Message F. 63 |  64 | 65 | Error: Message E. 66 |  67 | 68 | 0/13 tests passed. 69 | 70 | -------------------------------------------------------------------------------- /test/snapshots/TestDirector/test-fails-stdout.ans: -------------------------------------------------------------------------------- 1 | 2 | Test: a 3 | 4 | Test: b 5 | 6 | Test: c 7 | 8 | Test: d 9 | 10 | Test: e 11 | 12 | Test: f 13 | 14 | Test: g 15 | 16 | Test: h 17 | 18 | Test: i 19 | 20 | Test: j 21 | 22 | Cause: 23 | 24 | Cause: 25 | 26 | Test: k 27 | 28 | Cause: 29 | 30 | Test: l 31 | 32 | Test: m 33 | 34 | Aggregate errors: 35 | 36 | Cause: 37 | 38 | Aggregate errors: 39 | 40 | Cause: 41 | -------------------------------------------------------------------------------- /test/snapshots/TestDirector/test-nested-stderr.ans: -------------------------------------------------------------------------------- 1 | 2 | Error: Message. 3 |  4 | 5 | Error: 0/1 tests passed. 6 |  7 | 8 | 0/1 tests passed. 9 | 10 | -------------------------------------------------------------------------------- /test/snapshots/TestDirector/test-nested-stdout.ans: -------------------------------------------------------------------------------- 1 | 2 | Test: a 3 | 4 | Test: b 5 | -------------------------------------------------------------------------------- /test/snapshots/TestDirector/test-outputs-stdout.ans: -------------------------------------------------------------------------------- 1 | 2 | Test: a 3 | Message. 4 | 5 | 1/1 tests passed. 6 | 7 | -------------------------------------------------------------------------------- /test/snapshots/TestDirector/test-passes-stdout.ans: -------------------------------------------------------------------------------- 1 | 2 | Test: a 3 | 4 | 1/1 tests passed. 5 | 6 | -------------------------------------------------------------------------------- /test/snapshots/TestDirector/test-sequence-stdout.ans: -------------------------------------------------------------------------------- 1 | 2 | Test: a 3 | Message A. 4 | 5 | Test: b 6 | Message B. 7 | 8 | 2/2 tests passed. 9 | 10 | --------------------------------------------------------------------------------