├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── borp.js ├── fixtures ├── conf │ ├── glob-files.yaml │ ├── relative-reporter.yaml │ └── reporters.yaml ├── fails │ └── test │ │ └── wrong.test.js ├── files-glob │ ├── lib │ │ └── add.js │ ├── test1 │ │ └── add.test.js │ └── test2 │ │ └── nested │ │ └── add2.test.js ├── gc │ └── gc.test.js ├── js-esm │ ├── lib │ │ └── add.js │ └── test │ │ ├── add.test.js │ │ └── add2.test.js ├── long │ └── test │ │ └── long.test.js ├── monorepo │ ├── package-lock.json │ ├── package.json │ ├── package1 │ │ ├── package.json │ │ ├── src │ │ │ ├── lib │ │ │ │ └── add.ts │ │ │ └── test │ │ │ │ ├── add.test.ts │ │ │ │ └── add2.test.ts │ │ └── tsconfig.json │ ├── package2 │ │ ├── package.json │ │ ├── src │ │ │ ├── lib │ │ │ │ └── add.ts │ │ │ └── test │ │ │ │ ├── add.test.ts │ │ │ │ └── add2.test.ts │ │ └── tsconfig.json │ ├── tsconfig.base.json │ └── tsconfig.json ├── only-src │ ├── src │ │ ├── add.test.ts │ │ ├── add.ts │ │ └── add2.test.ts │ └── tsconfig.json ├── relative-reporter │ ├── lib │ │ └── add.js │ ├── reporter.js │ └── test │ │ ├── add.test.js │ │ └── add2.test.js ├── src-to-dist │ ├── src │ │ ├── lib │ │ │ └── add.ts │ │ └── test │ │ │ ├── add.test.ts │ │ │ └── add2.test.ts │ └── tsconfig.json ├── ts-cjs-post-compile │ ├── package.json │ ├── postCompile.ts │ ├── src │ │ └── add.ts │ ├── test │ │ ├── add.test.ts │ │ └── add2.test.ts │ └── tsconfig.json ├── ts-cjs │ ├── package.json │ ├── src │ │ └── add.ts │ ├── test │ │ ├── add.test.ts │ │ └── add2.test.ts │ └── tsconfig.json ├── ts-esm-check-coverage │ ├── src │ │ └── math.ts │ ├── test │ │ └── add.test.ts │ └── tsconfig.json ├── ts-esm-post-compile │ ├── postCompile.ts │ ├── src │ │ └── add.ts │ ├── test │ │ ├── add.test.ts │ │ └── add2.test.ts │ └── tsconfig.json ├── ts-esm-source-map │ ├── src │ │ └── add.ts │ ├── test │ │ └── add.test.ts │ └── tsconfig.json ├── ts-esm │ ├── src │ │ └── add.ts │ ├── test │ │ ├── add.test.ts │ │ └── add2.test.ts │ └── tsconfig.json └── ts-esm2 │ ├── src │ └── add.ts │ ├── test │ ├── add.test.ts │ └── add2.test.ts │ └── tsconfig.json ├── lib ├── conf.js └── run.js ├── package-lock.json ├── package.json ├── test-utils └── clock.js └── test ├── basic.test.js ├── cli.test.js ├── config.test.js ├── coverage.test.js ├── sourceMap.test.js ├── timeout.test.js └── watch.test.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: standard 10 | versions: 11 | - 16.0.3 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '*.md' 8 | pull_request: 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | 13 | jobs: 14 | test: 15 | runs-on: ${{matrix.os}} 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x, 21.x, 22.x, 23.x] 20 | os: [ubuntu-latest, windows-latest] 21 | exclude: 22 | - os: windows-latest 23 | node-version: 21.x 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Use Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | 32 | - name: Install 33 | run: | 34 | npm install 35 | 36 | - name: Lint 37 | run: | 38 | npm run lint 39 | 40 | - name: Run tests 41 | run: | 42 | npm run unit 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # tsimp 133 | .tsimp 134 | .test-watch* 135 | coverage-* 136 | 137 | # Jetbrains ide config 138 | .idea 139 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matteo Collina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # borp 2 | 3 | Borp is a typescript-aware test runner for `node:test`. 4 | It also support code coverage via [c8](http://npm.im/c8). 5 | 6 | Borp is self-hosted, i.e. Borp runs its own tests. 7 | 8 | ## Install 9 | 10 | ```bash 11 | npm i borp --save-dev 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```bash 17 | borp --coverage 18 | 19 | # with check coverage active 20 | borp --coverage --check-coverage --lines 95 21 | 22 | # with a node_modules located reporter 23 | borp --reporter foo 24 | 25 | # with a node_modules located reporter writing to stderr 26 | borp --reporter foo:stderr 27 | 28 | # with a local custom reporter 29 | borp --reporter ./lib/some-reporter.mjs 30 | 31 | # matching all test.js files except ones in nested node_modules directories 32 | borp 'test/**/*.test.js' '!test/**/node_modules/**/*.test.js' 33 | ``` 34 | 35 | Borp will automatically run all tests files matching `*.test.{js|ts}`. 36 | 37 | ### Example project setup 38 | 39 | ``` 40 | . 41 | ├── src 42 | │   ├── lib 43 | │   │   └── math.ts 44 | │   └── test 45 | │   └── math.test.ts 46 | └── tsconfig.json 47 | 48 | ``` 49 | 50 | As an example, consider having a `src/lib/math.ts` file 51 | 52 | ```typescript 53 | export function math (x: number, y: number): number { 54 | return x + y 55 | } 56 | ``` 57 | 58 | and a `src/test/math.test.ts` file: 59 | 60 | ```typescript 61 | import { test } from 'node:test' 62 | import { math } from '../lib/math.js' 63 | import { strictEqual } from 'node:assert' 64 | 65 | test('math', () => { 66 | strictEqual(math(1, 2), 3) 67 | }) 68 | ``` 69 | 70 | and the following `tsconfig.json`: 71 | 72 | ```json 73 | { 74 | "$schema": "https://json.schemastore.org/tsconfig", 75 | "compilerOptions": { 76 | "outDir": "dist", 77 | "sourceMap": true, 78 | "target": "ES2022", 79 | "module": "NodeNext", 80 | "moduleResolution": "NodeNext", 81 | "esModuleInterop": true, 82 | "strict": true, 83 | "resolveJsonModule": true, 84 | "removeComments": true, 85 | "newLine": "lf", 86 | "noUnusedLocals": true, 87 | "noFallthroughCasesInSwitch": true, 88 | "isolatedModules": true, 89 | "forceConsistentCasingInFileNames": true, 90 | "skipLibCheck": true, 91 | "lib": [ 92 | "ESNext" 93 | ], 94 | "incremental": true 95 | } 96 | } 97 | ``` 98 | 99 | Note the use of `incremental: true`, which speed up compilation massively. 100 | 101 | ## Options 102 | 103 | * `--concurrency` or `-c`, to set the number of concurrent tests. Defaults to the number of available CPUs minus one. 104 | * `--coverage` or `-C`, enables code coverage 105 | * `--only` or `-o`, only run `node:test` with the `only` option set 106 | * `--watch` or `-w`, re-run tests on changes 107 | * `--timeout` or `-t`, timeouts the tests after a given time; default is 30000 ms 108 | * `--no-timeout`, disables the timeout 109 | * `--coverage-exclude` or `-X`, a list of comma-separated patterns to exclude from the coverage report. All tests files are ignored by default. 110 | * `--ignore` or `-i`, ignore a glob pattern, and not look for tests there 111 | * `--expose-gc`, exposes the gc() function to tests 112 | * `--pattern` or `-p`, run tests matching the given glob pattern 113 | * `--reporter` or `-r`, set up a reporter, use a colon to set a file destination. Reporter may either be a module name resolvable by standard `node_modules` resolution, or a path to a script relative to the process working directory (must be an ESM script). Default: `spec`. 114 | * `--no-typescript` or `-T`, disable automatic TypeScript compilation if `tsconfig.json` is found. 115 | * `--post-compile` or `-P`, the path to a file that will be executed after each typescript compilation. 116 | * `--check-coverage`, enables c8 check coverage; default is false 117 | ### Check coverage options 118 | * `--lines`, set the lines threshold when check coverage is active; default is 100 119 | * `--functions`, set the functions threshold when check coverage is active; default is 100 120 | * `--statements`, set the statements threshold when check coverage is active; default is 100 121 | * `--branches`, set the branches threshold when check coverage is active; default is 100 122 | ## Reporters 123 | 124 | Here are the available reporters: 125 | 126 | * `gh`: emits `::error` workflow commands for GitHub Actions to show inlined errors. Enabled by default when running on GHA. 127 | * `tap`: outputs the test results in the TAP format. 128 | * `spec`: outputs the test results in a human-readable format. 129 | * `dot`: outputs the test results in a compact format, where each passing test is represented by a ., and each failing test is represented by a X. 130 | * `junit`: outputs test results in a jUnit XML format 131 | 132 | ## Config File Support 133 | 134 | A limited set of options may be specified via a configuration file. The 135 | configuration file is expected to be in the process's working directory, and 136 | named either `.borp.yaml` or `.borp.yml`; it may also be specified by 137 | defining the environment variable `BORP_CONF_FILE` and setting it to the 138 | full path to some yaml file. 139 | 140 | The current supported options are: 141 | 142 | + `files` (string[]): An array of test files to include. Globs are supported. 143 | Note: any glob that starts with a `!` (bang character) will be treated as 144 | an ignore glob, e.g. `'!test/**/node_modules/**/*'` will ignore all files 145 | in nested `node_modules` directories that would otherwise be matched. 146 | + `reporters` (string[]): An array of reporters to use. May be relative path 147 | strings, or module name strings. 148 | 149 | ### Example 150 | 151 | ```yaml 152 | files: 153 | - 'test/one.test.js' 154 | - 'test/foo/*.test.js' 155 | 156 | reporters: 157 | - './test/lib/my-reporter.js' 158 | - spec 159 | - '@reporters/silent' 160 | ``` 161 | 162 | ## License 163 | 164 | MIT 165 | -------------------------------------------------------------------------------- /borp.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import { parseArgs } from 'node:util' 4 | import Reporters from 'node:test/reporters' 5 | import { findUp } from 'find-up' 6 | import { mkdtemp, rm, readFile } from 'node:fs/promises' 7 | import { createWriteStream } from 'node:fs' 8 | import { finished } from 'node:stream/promises' 9 | import { join, relative, resolve } from 'node:path' 10 | import posix from 'node:path/posix' 11 | import runWithTypeScript from './lib/run.js' 12 | import githubReporter from '@reporters/github' 13 | import { Report } from 'c8' 14 | import { checkCoverages } from 'c8/lib/commands/check-coverage.js' 15 | import os from 'node:os' 16 | import { execa } from 'execa' 17 | import { pathToFileURL } from 'node:url' 18 | import loadConfig from './lib/conf.js' 19 | 20 | /* c8 ignore next 4 */ 21 | process.on('unhandledRejection', (err) => { 22 | console.error(err) 23 | process.exit(1) 24 | }) 25 | 26 | const foundConfig = await loadConfig() 27 | if (foundConfig.length > 0) { 28 | Array.prototype.push.apply(process.argv, foundConfig) 29 | } 30 | 31 | const args = parseArgs({ 32 | args: process.argv.slice(2), 33 | options: { 34 | only: { type: 'boolean', short: 'o' }, 35 | watch: { type: 'boolean', short: 'w' }, 36 | pattern: { type: 'string', short: 'p' }, 37 | concurrency: { type: 'string', short: 'c', default: (os.availableParallelism() - 1 || 1) + '' }, 38 | coverage: { type: 'boolean', short: 'C' }, 39 | timeout: { type: 'string', short: 't', default: '30000' }, 40 | 'no-timeout': { type: 'boolean' }, 41 | 'coverage-exclude': { type: 'string', short: 'X', multiple: true }, 42 | ignore: { type: 'string', short: 'i', multiple: true }, 43 | 'expose-gc': { type: 'boolean' }, 44 | help: { type: 'boolean', short: 'h' }, 45 | 'no-typescript': { type: 'boolean', short: 'T' }, 46 | 'post-compile': { type: 'string', short: 'P' }, 47 | reporter: { 48 | type: 'string', 49 | short: 'r', 50 | default: ['spec'], 51 | multiple: true 52 | }, 53 | 'check-coverage': { type: 'boolean' }, 54 | lines: { type: 'string', default: '100' }, 55 | branches: { type: 'string', default: '100' }, 56 | functions: { type: 'string', default: '100' }, 57 | statements: { type: 'string', default: '100' } 58 | }, 59 | allowPositionals: true 60 | }) 61 | 62 | /* c8 ignore next 5 */ 63 | if (args.values.help) { 64 | console.log(await readFile(new URL('./README.md', import.meta.url), 'utf8')) 65 | process.exit(0) 66 | } 67 | 68 | if (args.values['expose-gc'] && typeof global.gc !== 'function') { 69 | try { 70 | await execa('node', ['--expose-gc', ...process.argv.slice(1)], { 71 | stdio: 'inherit', 72 | env: { 73 | ...process.env 74 | } 75 | }) 76 | process.exit(0) 77 | } catch (error) { 78 | process.exit(1) 79 | } 80 | } 81 | 82 | if (args.values.concurrency) { 83 | args.values.concurrency = parseInt(args.values.concurrency) 84 | } 85 | 86 | if (args.values['no-timeout']) { 87 | delete args.values.timeout 88 | } 89 | 90 | if (args.values.timeout) { 91 | args.values.timeout = parseInt(args.values.timeout) 92 | } 93 | 94 | let covDir 95 | if (args.values.coverage) { 96 | covDir = await mkdtemp(join(os.tmpdir(), 'coverage-')) 97 | process.env.NODE_V8_COVERAGE = covDir 98 | } 99 | 100 | const config = { 101 | ...args.values, 102 | typescript: !args.values['no-typescript'], 103 | files: args.positionals, 104 | pattern: args.values.pattern, 105 | cwd: process.cwd() 106 | } 107 | 108 | try { 109 | const pipes = [] 110 | 111 | const reporters = { 112 | ...Reporters, 113 | gh: githubReporter 114 | } 115 | 116 | // If we're running in a GitHub action, adds the gh reporter 117 | // by default so that we can report failures to GitHub 118 | if (process.env.GITHUB_ACTION) { 119 | args.values.reporter.push('gh') 120 | } 121 | 122 | for (const input of args.values.reporter) { 123 | const [name, dest] = input.split(':') 124 | let Ctor 125 | if (Object.hasOwn(reporters, name) === true) { 126 | Ctor = reporters[name] 127 | } else { 128 | try { 129 | // Try to load a custom reporter from a file relative to the process. 130 | let modPath = resolve(join(process.cwd(), name.replace(/^['"]/, '').replace(/['"]$/, ''))) 131 | if (process.platform === 'win32') { 132 | // On Windows, absolute paths must be valid file:// URLs 133 | modPath = pathToFileURL(modPath).href 134 | } 135 | 136 | Ctor = await import(modPath).then((m) => m.default || m) 137 | } catch { 138 | // Fallback to trying to load the reporter from node_modules resolution. 139 | Ctor = await import(name).then((m) => m.default || m) 140 | } 141 | } 142 | const reporter = Ctor.prototype && Object.getOwnPropertyDescriptor(Ctor.prototype, 'constructor') ? new Ctor() : Ctor 143 | let output = process.stdout 144 | if (dest) { 145 | output = createWriteStream(dest) 146 | } 147 | pipes.push([reporter, output]) 148 | } 149 | 150 | const stream = await runWithTypeScript(config) 151 | 152 | stream.on('test:fail', () => { 153 | process.exitCode = 1 154 | }) 155 | 156 | for (const [reporter, output] of pipes) { 157 | stream.compose(reporter).pipe(output) 158 | } 159 | 160 | await finished(stream) 161 | 162 | if (covDir) { 163 | let exclude = args.values['coverage-exclude'] 164 | 165 | if (exclude && config.prefix) { 166 | const localPrefix = relative(process.cwd(), config.prefix) 167 | exclude = exclude.map((file) => posix.join(localPrefix, file)) 168 | } 169 | const nycrc = await findUp(['.c8rc', '.c8rc.json', '.nycrc', '.nycrc.json'], { cwd: config.cwd }) 170 | const report = Report({ 171 | reporter: ['text'], 172 | tempDirectory: covDir, 173 | exclude, 174 | ...nycrc && JSON.parse(await readFile(nycrc, 'utf8')) 175 | }) 176 | 177 | if (args.values['check-coverage']) { 178 | await checkCoverages({ 179 | lines: parseInt(args.values.lines), 180 | functions: parseInt(args.values.functions), 181 | branches: parseInt(args.values.branches), 182 | statements: parseInt(args.values.statements), 183 | ...args 184 | }, report) 185 | } 186 | await report.run() 187 | } 188 | /* c8 ignore next 3 */ 189 | } catch (err) { 190 | console.error(err) 191 | process.exitCode = 1 192 | } finally { 193 | if (covDir) { 194 | try { 195 | await rm(covDir, { recursive: true, maxRetries: 10, retryDelay: 100 }) 196 | /* c8 ignore next 2 */ 197 | } catch {} 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /fixtures/conf/glob-files.yaml: -------------------------------------------------------------------------------- 1 | files: 2 | - 'test1/*.test.js' 3 | - 'test2/**/*.test.js' -------------------------------------------------------------------------------- /fixtures/conf/relative-reporter.yaml: -------------------------------------------------------------------------------- 1 | reporters: 2 | - './reporter.js' -------------------------------------------------------------------------------- /fixtures/conf/reporters.yaml: -------------------------------------------------------------------------------- 1 | reporters: 2 | - spec 3 | - '@reporters/silent' -------------------------------------------------------------------------------- /fixtures/fails/test/wrong.test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert' 2 | import { test } from 'node:test' 3 | 4 | test('this will fail', () => { 5 | strictEqual(1, 2) 6 | }) 7 | -------------------------------------------------------------------------------- /fixtures/files-glob/lib/add.js: -------------------------------------------------------------------------------- 1 | export function add (x, y) { 2 | return x + y 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/files-glob/test1/add.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/files-glob/test2/nested/add2.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/gc/gc.test.js: -------------------------------------------------------------------------------- 1 | import { doesNotThrow } from 'node:assert' 2 | import { test } from 'node:test' 3 | 4 | test('this needs gc', () => { 5 | doesNotThrow(() => { 6 | global.gc() 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /fixtures/js-esm/lib/add.js: -------------------------------------------------------------------------------- 1 | export function add (x, y) { 2 | return x + y 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/js-esm/test/add.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/js-esm/test/add2.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/long/test/long.test.js: -------------------------------------------------------------------------------- 1 | import { ok } from 'node:assert' 2 | import { test } from 'node:test' 3 | 4 | test('this will take a long time', (t, done) => { 5 | setTimeout(() => { 6 | ok(true) 7 | done() 8 | }, 1e3) 9 | console.log('test:waiting') 10 | }) 11 | -------------------------------------------------------------------------------- /fixtures/monorepo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-build", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "project-build", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "workspaces": [ 12 | "package*" 13 | ] 14 | }, 15 | "node_modules/package1": { 16 | "resolved": "package1", 17 | "link": true 18 | }, 19 | "node_modules/package2": { 20 | "resolved": "package2", 21 | "link": true 22 | }, 23 | "package1": { 24 | "version": "1.0.0", 25 | "license": "ISC" 26 | }, 27 | "package2": { 28 | "version": "1.0.0", 29 | "license": "ISC", 30 | "dependencies": { 31 | "package1": "file:../package1" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /fixtures/monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-build", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "workspaces": [ 12 | "package*" 13 | ], 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/monorepo/package1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package1", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/src/lib/add.js", 6 | "types": "dist/src/lib/add.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC" 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/monorepo/package1/src/lib/add.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/monorepo/package1/src/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/monorepo/package1/src/test/add2.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/monorepo/package1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | "references": [ 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/monorepo/package2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package2", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "package1": "file:../package1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/monorepo/package2/src/lib/add.ts: -------------------------------------------------------------------------------- 1 | import { add as add2 } from 'package1' 2 | export function add (x: number, y: number): number { 3 | return add2(x, y) 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/monorepo/package2/src/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('package2-add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/monorepo/package2/src/test/add2.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('package2-add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/monorepo/package2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | "references": [ 8 | { "path": "../package1" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/monorepo/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "target": "ES2022", 6 | "composite": true, 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/monorepo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "package1" }, 5 | { "path": "package2" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/only-src/src/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from './add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/only-src/src/add.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/only-src/src/add2.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from './add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/only-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/relative-reporter/lib/add.js: -------------------------------------------------------------------------------- 1 | export function add (x, y) { 2 | return x + y 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/relative-reporter/reporter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Transform } from 'node:stream' 4 | 5 | const testReporter = new Transform({ 6 | writableObjectMode: true, 7 | transform (event, encoding, callback) { 8 | switch (event.type) { 9 | case 'test:pass': { 10 | return callback(null, `passed: ${event.data.file}\n`) 11 | } 12 | 13 | case 'test:fail': { 14 | return callback(null, `failed: ${event.data.file}\n`) 15 | } 16 | 17 | default: { 18 | callback(null, null) 19 | } 20 | } 21 | } 22 | }) 23 | 24 | export default testReporter 25 | -------------------------------------------------------------------------------- /fixtures/relative-reporter/test/add.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/relative-reporter/test/add2.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/src-to-dist/src/lib/add.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/src-to-dist/src/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/src-to-dist/src/test/add2.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../lib/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/src-to-dist/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/ts-cjs-post-compile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/ts-cjs-post-compile/postCompile.ts: -------------------------------------------------------------------------------- 1 | console.log('Doing stuff') -------------------------------------------------------------------------------- /fixtures/ts-cjs-post-compile/src/add.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/ts-cjs-post-compile/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-cjs-post-compile/test/add2.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-cjs-post-compile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/ts-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/ts-cjs/src/add.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/ts-cjs/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-cjs/test/add2.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-cjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/ts-esm-check-coverage/src/math.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | 6 | export function sub (x: number, y: number): number { 7 | return x - y 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/ts-esm-check-coverage/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/math.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-esm-check-coverage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/ts-esm-post-compile/postCompile.ts: -------------------------------------------------------------------------------- 1 | 2 | console.log('Doing stuff') -------------------------------------------------------------------------------- /fixtures/ts-esm-post-compile/src/add.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/ts-esm-post-compile/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-esm-post-compile/test/add2.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-esm-post-compile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/ts-esm-source-map/src/add.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/ts-esm-source-map/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-esm-source-map/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/ts-esm/src/add.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/ts-esm/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-esm/test/add2.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/ts-esm2/src/add.ts: -------------------------------------------------------------------------------- 1 | 2 | export function add (x: number, y: number): number { 3 | return x + y 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/ts-esm2/test/add.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add', () => { 6 | strictEqual(add(1, 2), 3) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-esm2/test/add2.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { add } from '../src/add.js' 3 | import { strictEqual } from 'node:assert' 4 | 5 | test('add2', () => { 6 | strictEqual(add(3, 2), 5) 7 | }) 8 | -------------------------------------------------------------------------------- /fixtures/ts-esm2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "removeComments": true, 13 | "newLine": "lf", 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "skipLibCheck": true, 19 | "lib": [ 20 | "ESNext" 21 | ], 22 | "incremental": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/conf.js: -------------------------------------------------------------------------------- 1 | import { cwd } from 'node:process' 2 | import { open, readFile } from 'node:fs/promises' 3 | import { join } from 'node:path' 4 | import YAML from 'yaml' 5 | 6 | async function readYamlFile () { 7 | let target 8 | let fd 9 | if (process.env.BORP_CONF_FILE) { 10 | target = process.env.BORP_CONF_FILE 11 | try { 12 | fd = await open(target, 'r') 13 | } catch { 14 | return 15 | } 16 | } else { 17 | const CWD = cwd() 18 | try { 19 | target = join(CWD, '.borp.yaml') 20 | fd = await open(target, 'r') 21 | } catch { 22 | target = join(CWD, '.borp.yml') 23 | try { 24 | fd = await open(target, 'r') 25 | } catch { 26 | // Neither file is available. If we had an application logger that writes 27 | // to stderr, we'd log an error message. But, as it is, we will just 28 | // assume that all errors are "file does not exist."" 29 | return 30 | } 31 | } 32 | } 33 | 34 | let fileData 35 | try { 36 | fileData = await readFile(fd, { encoding: 'utf8' }) 37 | } catch { 38 | // Same thing as noted above. Skip it. 39 | return 40 | } finally { 41 | await fd.close() 42 | } 43 | 44 | return fileData 45 | } 46 | 47 | async function loadConfig () { 48 | const result = [] 49 | const fileData = await readYamlFile() 50 | if (typeof fileData !== 'string') { 51 | return result 52 | } 53 | 54 | let options 55 | try { 56 | options = YAML.parse(fileData) 57 | } catch { 58 | // We just don't care. 59 | return result 60 | } 61 | 62 | if (options.reporters) { 63 | for (const reporter of options.reporters) { 64 | result.push('--reporter') 65 | result.push(reporter) 66 | } 67 | } 68 | 69 | // Append files AFTER all other supported config keys. The runner expects 70 | // them as positional parameters. 71 | if (options.files) { 72 | for (const file of options.files) { 73 | result.push(file) 74 | } 75 | } 76 | 77 | return result 78 | } 79 | 80 | export default loadConfig 81 | -------------------------------------------------------------------------------- /lib/run.js: -------------------------------------------------------------------------------- 1 | import { run } from 'node:test' 2 | import { glob } from 'glob' 3 | import { findUp } from 'find-up' 4 | import { createRequire } from 'node:module' 5 | import { join, dirname } from 'node:path' 6 | import { access } from 'node:fs/promises' 7 | import { execa } from 'execa' 8 | 9 | function deferred () { 10 | let resolve 11 | let reject 12 | const promise = new Promise((_resolve, _reject) => { 13 | resolve = _resolve 14 | reject = _reject 15 | }) 16 | return { resolve, reject, promise } 17 | } 18 | 19 | function enableSourceMapSupport (tsconfig) { 20 | if (!tsconfig?.options?.sourceMap) { 21 | return 22 | } 23 | 24 | process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + ' --enable-source-maps' 25 | } 26 | export default async function runWithTypeScript (config) { 27 | // This is a hack to override 28 | // https://github.com/nodejs/node/commit/d5c9adf3df 29 | delete process.env.NODE_TEST_CONTEXT 30 | 31 | const { cwd } = config 32 | let pushable = [] 33 | const tsconfigPath = await findUp('tsconfig.json', { cwd }) 34 | 35 | let prefix = '' 36 | let tscPath 37 | const typescriptCliArgs = [] 38 | 39 | let postCompileFn = async () => {} 40 | 41 | if (config['post-compile']) { 42 | postCompileFn = async (outDir) => { 43 | const postCompileFile = join(outDir ?? '', config['post-compile']).replace(/\.ts$/, '.js') 44 | const postCompileStart = Date.now() 45 | const { stdout } = await execa('node', [postCompileFile], { cwd: dirname(tsconfigPath) }) 46 | pushable.push({ 47 | type: 'test:diagnostic', 48 | data: { 49 | nesting: 0, 50 | message: `Post compile hook complete (${Date.now() - postCompileStart}ms)`, 51 | details: stdout, 52 | typescriptCliArgs 53 | } 54 | }) 55 | } 56 | } 57 | 58 | if (tsconfigPath && config.typescript !== false) { 59 | const _require = createRequire(tsconfigPath) 60 | const { parseJsonConfigFileContent, readConfigFile, sys } = _require('typescript') 61 | 62 | const configFile = readConfigFile(tsconfigPath, sys.readFile) 63 | const tsconfig = parseJsonConfigFileContent( 64 | configFile.config, 65 | sys, 66 | dirname(tsconfigPath) 67 | ) 68 | 69 | const typescriptPathCWD = _require.resolve('typescript') 70 | tscPath = join(typescriptPathCWD, '..', '..', 'bin', 'tsc') 71 | const outDir = tsconfig.options.outDir 72 | if (outDir) { 73 | prefix = outDir 74 | } 75 | 76 | enableSourceMapSupport(tsconfig) 77 | 78 | if (tscPath) { 79 | // This will throw if we cannot find the `tsc` binary 80 | await access(tscPath) 81 | 82 | // Watch is handled aftterwards 83 | if (!config.watch) { 84 | if (Array.isArray(tsconfig.projectReferences) && tsconfig.projectReferences.length > 0) { 85 | typescriptCliArgs.push('--build') 86 | } 87 | const start = Date.now() 88 | await execa('node', [tscPath, ...typescriptCliArgs], { cwd: dirname(tsconfigPath) }) 89 | process.stdout.write(`TypeScript compilation complete (${Date.now() - start}ms)\n`) 90 | pushable.push({ 91 | type: 'test:diagnostic', 92 | data: { 93 | nesting: 0, 94 | message: `TypeScript compilation complete (${Date.now() - start}ms)`, 95 | typescriptCliArgs 96 | } 97 | }) 98 | 99 | await postCompileFn(outDir) 100 | } 101 | } 102 | } 103 | 104 | // TODO remove those and create a new object 105 | delete config.typescript 106 | delete config['no-typescript'] 107 | 108 | config.prefix = prefix 109 | config.setup = (test) => { 110 | /* c8 ignore next 12 */ 111 | if (test.reporter) { 112 | for (const chunk of pushable) { 113 | test.reporter.push(chunk) 114 | } 115 | pushable = test.reporter 116 | } else { 117 | for (const chunk of pushable) { 118 | test.push(chunk) 119 | } 120 | pushable = test 121 | } 122 | } 123 | 124 | let tscChild 125 | /* eslint prefer-const: "off" */ 126 | let stream 127 | let p 128 | 129 | if (config.watch) { 130 | typescriptCliArgs.push('--watch') 131 | p = deferred() 132 | let outDir = '' 133 | if (config['post-compile'] && tsconfigPath) { 134 | const _require = createRequire(tsconfigPath) 135 | const { parseJsonConfigFileContent, readConfigFile, sys } = _require('typescript') 136 | 137 | const configFile = readConfigFile(tsconfigPath, sys.readFile) 138 | const tsconfig = parseJsonConfigFileContent( 139 | configFile.config, 140 | sys, 141 | dirname(tsconfigPath) 142 | ) 143 | 144 | outDir = tsconfig.options.outDir 145 | 146 | enableSourceMapSupport(tsconfig) 147 | } 148 | let start = Date.now() 149 | tscChild = execa('node', [tscPath, ...typescriptCliArgs], { cwd }) 150 | tscChild.stdout.setEncoding('utf8') 151 | tscChild.stdout.on('data', async (data) => { 152 | if (data.includes('File change detected')) { 153 | start = Date.now() 154 | } else if (data.includes('Watching for file changes')) { 155 | pushable.push({ 156 | type: 'test:diagnostic', 157 | data: { 158 | nesting: 0, 159 | message: `TypeScript compilation complete (${Date.now() - start}ms)`, 160 | typescriptCliArgs 161 | } 162 | }) 163 | 164 | await postCompileFn(outDir) 165 | 166 | p.resolve() 167 | } 168 | if (data.includes('error TS')) { 169 | pushable.push({ 170 | type: 'test:fail', 171 | data: { 172 | nesting: 0, 173 | name: data.trim() 174 | } 175 | }) 176 | } 177 | }) 178 | // We must noop `.catch()`, otherwise `tscChild` will 179 | // reject. 180 | tscChild.catch(() => {}) 181 | if (config.signal) { 182 | config.signal.addEventListener('abort', () => { 183 | tscChild.kill() 184 | }) 185 | } 186 | } 187 | 188 | if (p) { 189 | await p.promise 190 | } 191 | 192 | let files = config.files || [] 193 | const ignore = config.ignore || [] 194 | ignore.unshift('node_modules/**/*') 195 | if (files.length > 0) { 196 | if (prefix) { 197 | files = files.map((file) => join(prefix, file.replace(/ts$/, 'js'))) 198 | } 199 | 200 | const expandedFiles = [] 201 | const globs = [] 202 | for (let i = 0; i < files.length; i += 1) { 203 | if (files[i].includes('*') === false) { 204 | expandedFiles.push(files[i]) 205 | continue 206 | } 207 | const pattern = files[i].replace(/^['"]/, '').replace(/['"]$/, '') 208 | if (pattern[0] === '!') { 209 | ignore.push(pattern.slice(1)) 210 | continue 211 | } 212 | globs.push(pattern) 213 | } 214 | 215 | if (globs.length > 0) { 216 | const parsed = await glob(globs, { ignore, cwd, windowsPathsNoEscape: true }) 217 | Array.prototype.push.apply(expandedFiles, parsed) 218 | } 219 | 220 | files = expandedFiles 221 | } else if (config.pattern) { 222 | let pattern = config.pattern 223 | if (prefix) { 224 | pattern = join(prefix, pattern) 225 | pattern = pattern.replace(/ts$/, 'js') 226 | } 227 | files = await glob(pattern, { ignore, cwd, windowsPathsNoEscape: true }) 228 | } else if (prefix) { 229 | files = await glob(join(prefix, join('**', '*.test.{cjs,mjs,js}')), { ignore, cwd, windowsPathsNoEscape: true }) 230 | } else { 231 | files = await glob(join('**', '*.test.{cjs,mjs,js}'), { ignore, cwd, windowsPathsNoEscape: true, absolute: true }) 232 | } 233 | 234 | config.files = files 235 | 236 | config.coverage = false 237 | stream = run(config) 238 | 239 | stream.on('close', () => { 240 | if (tscChild) { 241 | tscChild.kill() 242 | } 243 | }) 244 | return stream 245 | } 246 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "borp", 3 | "version": "0.20.0", 4 | "type": "module", 5 | "description": "node:test wrapper with TypeScript support", 6 | "main": "borp.js", 7 | "bin": { 8 | "borp": "borp.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mcollina/borp" 13 | }, 14 | "scripts": { 15 | "clean": "rm -rf fixtures/*/dist .test-*", 16 | "lint": "standard | snazzy", 17 | "unit": "node borp.js --ignore \"fixtures/**/*\" -C --coverage-exclude \"fixtures/**/*\" --coverage-exclude \"test*/**/*\"", 18 | "test": "npm run clean ; npm run lint && npm run unit" 19 | }, 20 | "keywords": [], 21 | "author": "Matteo Collina ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@matteo.collina/tspl": "^0.1.0", 25 | "@reporters/silent": "^1.2.4", 26 | "@sinonjs/fake-timers": "^14.0.0", 27 | "@types/node": "^22.2.0", 28 | "desm": "^1.3.0", 29 | "semver": "^7.6.3", 30 | "snazzy": "^9.0.0", 31 | "standard": "^17.1.0", 32 | "typescript": "^5.3.2" 33 | }, 34 | "dependencies": { 35 | "@reporters/github": "^1.5.4", 36 | "c8": "^10.0.0", 37 | "execa": "^9.3.0", 38 | "find-up": "^7.0.0", 39 | "glob": "^10.3.10", 40 | "yaml": "^2.5.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test-utils/clock.js: -------------------------------------------------------------------------------- 1 | import FakeTimers from '@sinonjs/fake-timers' 2 | 3 | let clock 4 | 5 | if (process.argv[1].endsWith('borp.js')) { 6 | clock = FakeTimers.install({ 7 | now: Date.now(), 8 | shouldAdvanceTime: true, 9 | advanceTimeDelta: 100, 10 | toFake: ['Date', 'setTimeout', 'clearTimeout'] 11 | }) 12 | process.on('message', listener) 13 | } 14 | 15 | function listener ([fn, ...args]) { 16 | clock[fn](...args) 17 | if (fn === 'uninstall') { 18 | process.off('message', listener) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { tspl } from '@matteo.collina/tspl' 3 | import runWithTypeScript from '../lib/run.js' 4 | import { join } from 'desm' 5 | import { execa } from 'execa' 6 | 7 | test('ts-esm', async (t) => { 8 | const { strictEqual, completed, match } = tspl(t, { plan: 4 }) 9 | const config = { 10 | files: [], 11 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm') 12 | } 13 | 14 | const stream = await runWithTypeScript(config) 15 | 16 | const names = new Set(['add', 'add2']) 17 | 18 | stream.once('data', (test) => { 19 | strictEqual(test.type, 'test:diagnostic') 20 | match(test.data.message, /TypeScript compilation complete \(\d+ms\)/) 21 | }) 22 | 23 | stream.on('test:pass', (test) => { 24 | strictEqual(names.has(test.name), true) 25 | names.delete(test.name) 26 | }) 27 | 28 | await completed 29 | }) 30 | 31 | test('ts-cjs', async (t) => { 32 | const { strictEqual, completed } = tspl(t, { plan: 2 }) 33 | const config = { 34 | files: [], 35 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-cjs') 36 | } 37 | 38 | const stream = await runWithTypeScript(config) 39 | 40 | const names = new Set(['add', 'add2']) 41 | 42 | stream.on('test:pass', (test) => { 43 | strictEqual(names.has(test.name), true) 44 | names.delete(test.name) 45 | }) 46 | 47 | await completed 48 | }) 49 | 50 | test('ts-esm with named files', async (t) => { 51 | const { strictEqual, completed, match } = tspl(t, { plan: 3 }) 52 | const config = { 53 | files: ['test/add.test.ts'], 54 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm') 55 | } 56 | 57 | const stream = await runWithTypeScript(config) 58 | 59 | const names = new Set(['add']) 60 | 61 | stream.once('data', (test) => { 62 | strictEqual(test.type, 'test:diagnostic') 63 | match(test.data.message, /TypeScript compilation complete \(\d+ms\)/) 64 | }) 65 | 66 | stream.on('test:pass', (test) => { 67 | strictEqual(names.has(test.name), true) 68 | names.delete(test.name) 69 | }) 70 | 71 | await completed 72 | }) 73 | 74 | test('pattern', async (t) => { 75 | const { strictEqual, completed, match } = tspl(t, { plan: 3 }) 76 | const config = { 77 | files: [], 78 | pattern: 'test/*2.test.ts', 79 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm') 80 | } 81 | 82 | const stream = await runWithTypeScript(config) 83 | 84 | const names = new Set(['add2']) 85 | 86 | stream.once('data', (test) => { 87 | strictEqual(test.type, 'test:diagnostic') 88 | match(test.data.message, /TypeScript compilation complete \(\d+ms\)/) 89 | }) 90 | 91 | stream.on('test:pass', (test) => { 92 | strictEqual(names.has(test.name), true) 93 | names.delete(test.name) 94 | }) 95 | 96 | await completed 97 | }) 98 | 99 | test('no files', async (t) => { 100 | const { strictEqual, completed, match } = tspl(t, { plan: 4 }) 101 | const config = { 102 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm') 103 | } 104 | 105 | const stream = await runWithTypeScript(config) 106 | 107 | const names = new Set(['add', 'add2']) 108 | 109 | stream.once('data', (test) => { 110 | strictEqual(test.type, 'test:diagnostic') 111 | match(test.data.message, /TypeScript compilation complete \(\d+ms\)/) 112 | }) 113 | 114 | stream.on('test:pass', (test) => { 115 | strictEqual(names.has(test.name), true) 116 | names.delete(test.name) 117 | }) 118 | 119 | await completed 120 | }) 121 | 122 | test('src-to-dist', async (t) => { 123 | const { strictEqual, completed, match } = tspl(t, { plan: 4 }) 124 | const config = { 125 | files: [], 126 | cwd: join(import.meta.url, '..', 'fixtures', 'src-to-dist') 127 | } 128 | 129 | const stream = await runWithTypeScript(config) 130 | 131 | const names = new Set(['add', 'add2']) 132 | 133 | stream.once('data', (test) => { 134 | strictEqual(test.type, 'test:diagnostic') 135 | match(test.data.message, /TypeScript compilation complete \(\d+ms\)/) 136 | }) 137 | 138 | stream.on('test:pass', (test) => { 139 | strictEqual(names.has(test.name), true) 140 | names.delete(test.name) 141 | }) 142 | 143 | await completed 144 | }) 145 | test('monorepo', async (t) => { 146 | const { strictEqual, completed, match, deepEqual } = tspl(t, { plan: 5 }) 147 | const config = { 148 | files: [], 149 | cwd: join(import.meta.url, '..', 'fixtures', 'monorepo/package2') 150 | } 151 | 152 | await execa('npm', ['install'], { cwd: join(import.meta.url, '..', 'fixtures', 'monorepo') }) 153 | const stream = await runWithTypeScript(config) 154 | 155 | const names = new Set(['package2-add', 'package2-add2']) 156 | 157 | stream.once('data', (test) => { 158 | strictEqual(test.type, 'test:diagnostic') 159 | match(test.data.message, /TypeScript compilation complete \(\d+ms\)/) 160 | deepEqual(test.data.typescriptCliArgs, ['--build']) 161 | }) 162 | 163 | stream.on('test:pass', (test) => { 164 | strictEqual(names.has(test.name), true) 165 | names.delete(test.name) 166 | }) 167 | 168 | await completed 169 | }) 170 | test('only-src', async (t) => { 171 | const { strictEqual, completed, match } = tspl(t, { plan: 4 }) 172 | const config = { 173 | files: [], 174 | cwd: join(import.meta.url, '..', 'fixtures', 'only-src') 175 | } 176 | 177 | const stream = await runWithTypeScript(config) 178 | 179 | const names = new Set(['add', 'add2']) 180 | 181 | stream.once('data', (test) => { 182 | strictEqual(test.type, 'test:diagnostic') 183 | match(test.data.message, /TypeScript compilation complete \(\d+ms\)/) 184 | }) 185 | 186 | stream.on('test:pass', (test) => { 187 | strictEqual(names.has(test.name), true) 188 | names.delete(test.name) 189 | }) 190 | 191 | await completed 192 | }) 193 | 194 | test('js-esm', async (t) => { 195 | const { strictEqual, completed } = tspl(t, { plan: 2 }) 196 | const config = { 197 | files: [], 198 | cwd: join(import.meta.url, '..', 'fixtures', 'js-esm') 199 | } 200 | 201 | const stream = await runWithTypeScript(config) 202 | 203 | const names = new Set(['add', 'add2']) 204 | 205 | stream.on('test:pass', (test) => { 206 | strictEqual(names.has(test.name), true) 207 | names.delete(test.name) 208 | }) 209 | 210 | stream.resume() 211 | 212 | await completed 213 | }) 214 | -------------------------------------------------------------------------------- /test/cli.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { execa } from 'execa' 3 | import { join } from 'desm' 4 | import { rejects, strictEqual } from 'node:assert' 5 | import { rm } from 'node:fs/promises' 6 | import path from 'node:path' 7 | 8 | const borp = join(import.meta.url, '..', 'borp.js') 9 | 10 | delete process.env.GITHUB_ACTION 11 | 12 | test('limit concurrency', async () => { 13 | await execa('node', [ 14 | borp, 15 | '--concurrency', 16 | '1' 17 | ], { 18 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm') 19 | }) 20 | }) 21 | 22 | test('failing test set correct status code', async () => { 23 | // execa rejects if status code is not 0 24 | await rejects(execa('node', [ 25 | borp 26 | ], { 27 | cwd: join(import.meta.url, '..', 'fixtures', 'fails') 28 | })) 29 | }) 30 | 31 | test('--expose-gc flag enables garbage collection in tests', async () => { 32 | await execa('node', [ 33 | borp, 34 | '--expose-gc' 35 | ], { 36 | cwd: join(import.meta.url, '..', 'fixtures', 'gc') 37 | }) 38 | }) 39 | 40 | test('failing test with --expose-gc flag sets correct status code', async () => { 41 | // execa rejects if status code is not 0 42 | await rejects(execa('node', [ 43 | borp, 44 | '--expose-gc' 45 | ], { 46 | cwd: join(import.meta.url, '..', 'fixtures', 'fails') 47 | })) 48 | }) 49 | 50 | test('disable ts and run no tests', async () => { 51 | const cwd = join(import.meta.url, '..', 'fixtures', 'ts-esm2') 52 | await rm(path.join(cwd, 'dist'), { recursive: true, force: true }) 53 | const { stdout } = await execa('node', [ 54 | borp, 55 | '--reporter=spec', 56 | '--no-typescript' 57 | ], { 58 | cwd 59 | }) 60 | 61 | strictEqual(stdout.indexOf('tests 0') >= 0, true) 62 | }) 63 | 64 | test('reporter from node_modules', async () => { 65 | const cwd = join(import.meta.url, '..', 'fixtures', 'ts-esm') 66 | const { stdout } = await execa('node', [ 67 | borp, 68 | '--reporter=spec', 69 | '--reporter=@reporters/silent' 70 | ], { 71 | cwd 72 | }) 73 | 74 | strictEqual(stdout.indexOf('tests 2') >= 0, true) 75 | }) 76 | 77 | test('reporter from relative path', async () => { 78 | const cwd = join(import.meta.url, '..', 'fixtures', 'relative-reporter') 79 | const { stdout } = await execa('node', [ 80 | borp, 81 | '--reporter=./fixtures/relative-reporter/reporter.js' 82 | ], { 83 | cwd 84 | }) 85 | 86 | strictEqual(/passed:.+add\.test\.js/.test(stdout), true) 87 | strictEqual(/passed:.+add2\.test\.js/.test(stdout), true) 88 | }) 89 | 90 | test('gh reporter', async () => { 91 | const cwd = join(import.meta.url, '..', 'fixtures', 'js-esm') 92 | const { stdout } = await execa('node', [ 93 | borp, 94 | '--reporter=gh' 95 | ], { 96 | cwd, 97 | env: { 98 | GITHUB_ACTIONS: '1' 99 | } 100 | }) 101 | 102 | strictEqual(stdout.indexOf('::notice') >= 0, true) 103 | }) 104 | 105 | test('interprets globs for files', async () => { 106 | const cwd = join(import.meta.url, '..', 'fixtures', 'files-glob') 107 | const { stdout } = await execa('node', [ 108 | borp, 109 | '\'test1/*.test.js\'', 110 | '\'test2/**/*.test.js\'' 111 | ], { 112 | cwd 113 | }) 114 | 115 | strictEqual(stdout.indexOf('✔ add') >= 0, true) 116 | strictEqual(stdout.indexOf('✔ add2') >= 0, true) 117 | strictEqual(stdout.indexOf('✔ a thing'), -1) 118 | }) 119 | 120 | test('interprets globs for files with an ignore rule', async () => { 121 | const cwd = join(import.meta.url, '..', 'fixtures', 'files-glob') 122 | const { stdout } = await execa('node', [ 123 | borp, 124 | '\'**/*.test.js\'', 125 | '\'!test1/**/node_modules/**/*\'' 126 | ], { 127 | cwd 128 | }) 129 | 130 | strictEqual(stdout.indexOf('✔ add') >= 0, true) 131 | strictEqual(stdout.indexOf('✔ add2') >= 0, true) 132 | strictEqual(stdout.indexOf('✔ a thing'), -1) 133 | }) 134 | 135 | test('Post compile script should be executed when --post-compile is sent with esm', async () => { 136 | const cwd = join(import.meta.url, '..', 'fixtures', 'ts-esm-post-compile') 137 | const { stdout } = await execa('node', [ 138 | borp, 139 | '--post-compile=postCompile.ts' 140 | ], { 141 | cwd 142 | }) 143 | 144 | strictEqual(stdout.indexOf('Post compile hook complete') >= 0, true, 'Post compile message should be found in stdout') 145 | }) 146 | 147 | test('Post compile script should be executed when --post-compile is sent with cjs', async () => { 148 | const { stdout } = await execa('node', [ 149 | borp, 150 | '--post-compile=postCompile.ts' 151 | ], { 152 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-cjs-post-compile') 153 | }) 154 | 155 | strictEqual(stdout.indexOf('Post compile hook complete') >= 0, true, 'Post compile message should be found in stdout') 156 | }) 157 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { execa } from 'execa' 3 | import { join } from 'desm' 4 | import { strictEqual } from 'node:assert' 5 | import path from 'node:path' 6 | 7 | const borp = join(import.meta.url, '..', 'borp.js') 8 | const confFilesDir = join(import.meta.url, '..', 'fixtures', 'conf') 9 | 10 | test('reporter from node_modules', async () => { 11 | const cwd = join(import.meta.url, '..', 'fixtures', 'ts-esm') 12 | const { stdout } = await execa('node', [borp], { 13 | cwd, 14 | env: { 15 | BORP_CONF_FILE: path.join(confFilesDir, 'reporters.yaml') 16 | } 17 | }) 18 | 19 | strictEqual(stdout.indexOf('tests 2') >= 0, true) 20 | }) 21 | 22 | test('reporter from relative path', async () => { 23 | const cwd = join(import.meta.url, '..', 'fixtures', 'relative-reporter') 24 | const { stdout } = await execa('node', [borp], { 25 | cwd, 26 | env: { 27 | BORP_CONF_FILE: path.join(confFilesDir, 'relative-reporter.yaml') 28 | } 29 | }) 30 | 31 | strictEqual(/passed:.+add\.test\.js/.test(stdout), true) 32 | strictEqual(/passed:.+add2\.test\.js/.test(stdout), true) 33 | }) 34 | 35 | test('interprets globs for files', async () => { 36 | const cwd = join(import.meta.url, '..', 'fixtures', 'files-glob') 37 | const { stdout } = await execa('node', [borp], { 38 | cwd, 39 | env: { 40 | BORP_CONF_FILE: path.join(confFilesDir, 'glob-files.yaml') 41 | } 42 | }) 43 | 44 | strictEqual(stdout.indexOf('tests 2') >= 0, true) 45 | }) 46 | -------------------------------------------------------------------------------- /test/coverage.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { match, doesNotMatch, fail, equal, AssertionError } from 'node:assert' 3 | import { execa } from 'execa' 4 | import { join } from 'desm' 5 | 6 | delete process.env.GITHUB_ACTION 7 | const borp = join(import.meta.url, '..', 'borp.js') 8 | 9 | test('coverage', async () => { 10 | const res = await execa('node', [ 11 | borp, 12 | '--coverage' 13 | ], { 14 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm') 15 | }) 16 | 17 | match(res.stdout, /% Stmts/) 18 | match(res.stdout, /All files/) 19 | match(res.stdout, /add\.ts/) 20 | }) 21 | 22 | test('coverage excludes', async () => { 23 | const res = await execa('node', [ 24 | borp, 25 | '-C', 26 | '--coverage-exclude=src' 27 | ], { 28 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm') 29 | }) 30 | 31 | match(res.stdout, /% Stmts/) 32 | match(res.stdout, /All files/) 33 | doesNotMatch(res.stdout, /add\.ts/) 34 | // The test files are shown 35 | match(res.stdout, /add\.test\.ts/) 36 | match(res.stdout, /add2\.test\.ts/) 37 | }) 38 | 39 | test('borp should return right error when check coverage is active with default thresholds', async (t) => { 40 | try { 41 | await execa('node', [ 42 | borp, 43 | '--coverage', 44 | '--check-coverage' 45 | ], { 46 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm-check-coverage') 47 | }) 48 | fail('Should not complete borp without error') 49 | } catch (e) { 50 | if (e instanceof AssertionError) { 51 | throw e 52 | } 53 | 54 | equal(e.exitCode, 1) 55 | match(e.stderr, /ERROR: Coverage for lines \(75%\) does not meet global threshold \(100%\)/) 56 | match(e.stderr, /ERROR: Coverage for functions \(50%\) does not meet global threshold \(100%\)/) 57 | match(e.stderr, /ERROR: Coverage for statements \(75%\) does not meet global threshold \(100%\)/) 58 | } 59 | }) 60 | 61 | test('borp should return right error when check coverage is active with defined thresholds', async (t) => { 62 | try { 63 | await execa('node', [ 64 | borp, 65 | '--coverage', 66 | '--check-coverage', 67 | '--lines=80', 68 | '--functions=50', 69 | '--statements=0', 70 | '--branches=100' 71 | ], { 72 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm-check-coverage') 73 | }) 74 | fail('Should not complete borp without error') 75 | } catch (e) { 76 | if (e instanceof AssertionError) { 77 | throw e 78 | } 79 | 80 | equal(e.exitCode, 1) 81 | match(e.stderr, /ERROR: Coverage for lines \(75%\) does not meet global threshold \(80%\)/) 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /test/sourceMap.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { test } from 'node:test' 4 | import { execa } from 'execa' 5 | import { existsSync } from 'node:fs' 6 | import { AssertionError, equal, fail, match, strictEqual } from 'node:assert' 7 | import { tspl } from '@matteo.collina/tspl' 8 | import path from 'node:path' 9 | import { cp, mkdtemp, rm, writeFile } from 'node:fs/promises' 10 | import runWithTypeScript from '../lib/run.js' 11 | import { join } from 'desm' 12 | 13 | test('borp should return ts file path in error message when sourceMap is active in tsconfig', async (t) => { 14 | const borp = join(import.meta.url, '..', 'borp.js') 15 | 16 | try { 17 | await execa('node', [ 18 | borp 19 | ], { 20 | cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm-source-map') 21 | }) 22 | fail('Should not complete borp without error') 23 | } catch (e) { 24 | if (e instanceof AssertionError) { 25 | throw e 26 | } 27 | 28 | equal(e.exitCode, 1) 29 | match(e.message, /at TestContext\..*add\.test\.ts/, 'Error message should contain ts file path') 30 | } 31 | 32 | const fileMapPath = join(import.meta.url, '..', 'fixtures', 'ts-esm-source-map', 'dist', 'src', 'add.js.map') 33 | strictEqual(existsSync(fileMapPath), true, 'Map file should be generated') 34 | }) 35 | 36 | test('source map should be generated when watch is active', async (t) => { 37 | const { strictEqual, completed } = tspl(t, { plan: 1 }) 38 | 39 | const dir = path.resolve(await mkdtemp('.test-watch')) 40 | await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm-source-map'), dir, { 41 | recursive: true 42 | }) 43 | 44 | const passTest = ` 45 | import { test } from 'node:test' 46 | import { add } from '../src/add.js' 47 | import { strictEqual } from 'node:assert' 48 | 49 | test('addSuccess', () => { 50 | strictEqual(add(1,2), 3) 51 | }) 52 | ` 53 | await writeFile(path.join(dir, 'test', 'add.test.ts'), passTest) 54 | 55 | const controller = new AbortController() 56 | t.after(async () => { 57 | controller.abort() 58 | try { 59 | await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 }) 60 | } catch {} 61 | }) 62 | 63 | const config = { 64 | files: [], 65 | cwd: dir, 66 | signal: controller.signal, 67 | watch: true 68 | } 69 | 70 | await runWithTypeScript(config) 71 | 72 | const failureTestToWrite = ` 73 | import { test } from 'node:test' 74 | import { add } from '../src/add.js' 75 | import { strictEqual } from 'node:assert' 76 | 77 | test('addFailure', () => { 78 | strictEqual(add(1,2), 4) 79 | }) 80 | ` 81 | await writeFile(path.join(dir, 'test', 'add.test.ts'), failureTestToWrite) 82 | 83 | const expectedMapFilePath = path.join(dir, 'dist', 'src', 'add.js.map') 84 | strictEqual(existsSync(expectedMapFilePath), true, 'Map file should be generated') 85 | 86 | await completed 87 | }) 88 | -------------------------------------------------------------------------------- /test/timeout.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { once } from 'node:events' 3 | import { pathToFileURL } from 'node:url' 4 | import { fork } from 'node:child_process' 5 | import { tspl } from '@matteo.collina/tspl' 6 | import { join } from 'desm' 7 | 8 | const borp = join(import.meta.url, '..', 'borp.js') 9 | const clock = join(import.meta.url, '..', 'test-utils', 'clock.js') 10 | const forkOpts = { 11 | cwd: join(import.meta.url, '..', 'fixtures', 'long'), 12 | env: { NODE_OPTIONS: `--import=${pathToFileURL(clock)}` }, 13 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 14 | } 15 | 16 | test('times out after 30s by default', async (t) => { 17 | const { ok, equal } = tspl(t, { plan: 4 }) 18 | const borpProcess = fork(borp, forkOpts) 19 | let stdout = '' 20 | borpProcess.stdout.on('data', (data) => { 21 | stdout += data 22 | if (data.includes('test:waiting')) { 23 | borpProcess.send(['tick', 30e3]) 24 | borpProcess.send(['uninstall']) 25 | } 26 | }) 27 | const [code] = await once(borpProcess, 'exit') 28 | equal(code, 1) 29 | ok(stdout.includes('test timed out after 30000ms')) 30 | ok(stdout.includes('tests 1')) 31 | ok(stdout.includes('cancelled 1')) 32 | }) 33 | 34 | test('does not timeout when setting --no-timeout', async (t) => { 35 | const { ok, equal } = tspl(t, { plan: 4 }) 36 | const borpProcess = fork(borp, ['--no-timeout'], forkOpts) 37 | borpProcess.stderr.pipe(process.stderr) 38 | let stdout = '' 39 | borpProcess.stdout.on('data', (data) => { 40 | stdout += data 41 | if (data.includes('test:waiting')) { 42 | borpProcess.send(['tick', 30e3]) 43 | borpProcess.send(['uninstall']) 44 | } 45 | }) 46 | const [code] = await once(borpProcess, 'exit') 47 | equal(code, 0) 48 | ok(stdout.includes('✔ this will take a long time')) 49 | ok(stdout.includes('tests 1')) 50 | ok(stdout.includes('pass 1')) 51 | }) 52 | 53 | test('timeout is configurable', async (t) => { 54 | const { ok, equal } = tspl(t, { plan: 4 }) 55 | const borpProcess = fork(borp, ['--timeout', '10000'], forkOpts) 56 | let stdout = '' 57 | borpProcess.stdout.on('data', (data) => { 58 | stdout += data 59 | if (data.includes('test:waiting')) { 60 | borpProcess.send(['tick', 10e3]) 61 | borpProcess.send(['uninstall']) 62 | } 63 | }) 64 | const [code] = await once(borpProcess, 'exit') 65 | equal(code, 1) 66 | ok(stdout.includes('test timed out after 10000ms')) 67 | ok(stdout.includes('tests 1')) 68 | ok(stdout.includes('cancelled 1')) 69 | }) 70 | -------------------------------------------------------------------------------- /test/watch.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import { tspl } from '@matteo.collina/tspl' 3 | import runWithTypeScript from '../lib/run.js' 4 | import { join } from 'desm' 5 | import { mkdtemp, cp, writeFile, rm } from 'node:fs/promises' 6 | import path from 'node:path' 7 | import { once } from 'node:events' 8 | import semver from 'semver' 9 | 10 | // These tests are currently broken on some node versions 11 | const skip = process.platform === 'darwin' && semver.satisfies(process.version, '>=20.16.0 <22.10.0') 12 | 13 | test('watch', { skip }, async (t) => { 14 | const { strictEqual, completed, match } = tspl(t, { plan: 3 }) 15 | 16 | const dir = path.resolve(await mkdtemp('.test-watch')) 17 | await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm'), dir, { 18 | recursive: true 19 | }) 20 | 21 | const controller = new AbortController() 22 | t.after(async () => { 23 | controller.abort() 24 | try { 25 | await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 }) 26 | } catch {} 27 | }) 28 | 29 | const config = { 30 | files: [], 31 | signal: controller.signal, 32 | cwd: dir, 33 | watch: true 34 | } 35 | 36 | process._rawDebug('dir', dir) 37 | const stream = await runWithTypeScript(config) 38 | 39 | const fn = (test) => { 40 | if (test.type === 'test:fail') { 41 | console.log('test', test) 42 | match(test.data.name, /add/) 43 | stream.removeListener('data', fn) 44 | } 45 | } 46 | stream.on('data', fn) 47 | 48 | const [test] = await once(stream, 'data') 49 | strictEqual(test.type, 'test:diagnostic') 50 | match(test.data.message, /TypeScript compilation complete \(\d+ms\)/) 51 | 52 | const toWrite = ` 53 | import { test } from 'node:test' 54 | import { add } from '../src/add.js' 55 | import { strictEqual } from 'node:assert' 56 | 57 | test('add', () => { 58 | strictEqual(add(1, 2), 4) 59 | }) 60 | ` 61 | const file = path.join(dir, 'test', 'add.test.ts') 62 | await writeFile(file, toWrite) 63 | 64 | await completed 65 | }) 66 | 67 | test('watch file syntax error', { skip }, async (t) => { 68 | const { strictEqual, completed, match } = tspl(t, { plan: 3 }) 69 | 70 | const dir = path.resolve(await mkdtemp('.test-watch')) 71 | await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm'), dir, { 72 | recursive: true 73 | }) 74 | 75 | const controller = new AbortController() 76 | t.after(async () => { 77 | controller.abort() 78 | try { 79 | await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 }) 80 | } catch {} 81 | }) 82 | 83 | const config = { 84 | files: [], 85 | cwd: dir, 86 | signal: controller.signal, 87 | watch: true 88 | } 89 | 90 | const stream = await runWithTypeScript(config) 91 | 92 | const fn = (test) => { 93 | if (test.type === 'test:fail') { 94 | match(test.data.name, /add/) 95 | stream.removeListener('data', fn) 96 | } 97 | } 98 | stream.on('data', fn) 99 | 100 | const [test] = await once(stream, 'data') 101 | strictEqual(test.type, 'test:diagnostic') 102 | match(test.data.message, /TypeScript compilation complete \(\d+ms\)/) 103 | 104 | const toWrite = ` 105 | import { test } from 'node:test' 106 | import { add } from '../src/add.js' 107 | import { strictEqual } from 'node:assert' 108 | 109 | test('add', () => { 110 | strictEqual(add(1, 2), 3 111 | }) 112 | ` 113 | const file = path.join(dir, 'test', 'add.test.ts') 114 | await writeFile(file, toWrite) 115 | 116 | await completed 117 | }) 118 | 119 | test('watch with post compile hook should call the hook the right number of times', { skip }, async (t) => { 120 | const { completed, ok, match } = tspl(t, { plan: 2 }) 121 | 122 | const dir = path.resolve(await mkdtemp('.test-watch-with-post-compile-hook')) 123 | await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm-post-compile'), dir, { 124 | recursive: true 125 | }) 126 | 127 | const controller = new AbortController() 128 | t.after(async () => { 129 | controller.abort() 130 | try { 131 | await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 }) 132 | } catch {} 133 | }) 134 | 135 | const config = { 136 | 'post-compile': 'postCompile.ts', 137 | files: [], 138 | cwd: dir, 139 | signal: controller.signal, 140 | watch: true 141 | } 142 | 143 | const stream = await runWithTypeScript(config) 144 | 145 | const fn = (test) => { 146 | if (test.type === 'test:fail') { 147 | match(test.data.name, /add/) 148 | stream.removeListener('data', fn) 149 | } 150 | } 151 | stream.on('data', fn) 152 | 153 | let postCompileEventCount = 0 154 | const diagnosticListenerFn = (test) => { 155 | if (test.type === 'test:diagnostic' && test.data.message.includes('Post compile hook complete')) { 156 | if (++postCompileEventCount === 2) { 157 | ok(true, 'Post compile hook ran twice') 158 | stream.removeListener('data', diagnosticListenerFn) 159 | } 160 | } 161 | } 162 | 163 | stream.on('data', diagnosticListenerFn) 164 | 165 | const toWrite = ` 166 | import { test } from 'node:test' 167 | import { add } from '../src/add.js' 168 | import { strictEqual } from 'node:assert' 169 | 170 | test('add', () => { 171 | strictEqual(add(1, 2), 4) 172 | }) 173 | ` 174 | const file = path.join(dir, 'test', 'add.test.ts') 175 | await writeFile(file, toWrite) 176 | 177 | await completed 178 | }) 179 | --------------------------------------------------------------------------------