borp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,35 @@
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]
20
+ os: [ubuntu-latest, windows-latest]
21
+ steps:
22
+ - uses: actions/checkout@v3
23
+
24
+ - name: Use Node.js
25
+ uses: actions/setup-node@v2
26
+ with:
27
+ node-version: ${{ matrix.node-version }}
28
+
29
+ - name: Install
30
+ run: |
31
+ npm install
32
+
33
+ - name: Run tests
34
+ run: |
35
+ npm run test
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # borp
2
+
3
+ Borp is a typescript-aware runner for tests written using `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
+
20
+ Borp will autumatically run all tests files matching `*.test.{js|ts}`.
21
+
22
+ ### Example project setup
23
+
24
+ As an example, consider having a `src/add.ts` file
25
+
26
+ ```typescript
27
+ export function add (x: number, y: number): number {
28
+ return x + y
29
+ }
30
+ ```
31
+
32
+ and a `test/add.test.ts` file:
33
+
34
+ ```typescript
35
+ import { test } from 'node:test'
36
+ import { add } from '../src/add.js'
37
+ import { strictEqual } from 'node:assert'
38
+
39
+ test('add', () => {
40
+ strictEqual(add(1, 2), 3)
41
+ })
42
+ ```
43
+
44
+ and the following `tsconfig`:
45
+
46
+ ```json
47
+ {
48
+ "$schema": "https://json.schemastore.org/tsconfig",
49
+ "compilerOptions": {
50
+ "outDir": "dist",
51
+ "sourceMap": true,
52
+ "target": "ES2022",
53
+ "module": "NodeNext",
54
+ "moduleResolution": "NodeNext",
55
+ "esModuleInterop": true,
56
+ "strict": true,
57
+ "resolveJsonModule": true,
58
+ "removeComments": true,
59
+ "newLine": "lf",
60
+ "noUnusedLocals": true,
61
+ "noFallthroughCasesInSwitch": true,
62
+ "isolatedModules": true,
63
+ "forceConsistentCasingInFileNames": true,
64
+ "skipLibCheck": true,
65
+ "lib": [
66
+ "ESNext"
67
+ ],
68
+ "incremental": true
69
+ }
70
+ }
71
+ ```
72
+
73
+ Note the use of `incremental: true`, which speed up compilation massively.
74
+
75
+ ## Options
76
+
77
+ * `--coverage` or `-C`, enables code coverage
78
+ * `--only` or `-o`, only run `node:test` with the `only` option set
79
+ * `--watch` or `-w`, re-run tests on changes
80
+ * `--timeout` or `-t`, timeouts the tests after a given time; default is 30000 ms
81
+ * `--coverage-exclude` or `-X`, a list of comma-separated patterns to exclude from the coverage report. All tests files are ignored by default.
82
+
83
+ ## License
84
+
85
+ MIT
package/borp.js CHANGED
@@ -2,27 +2,18 @@
2
2
 
3
3
  import { parseArgs } from 'node:util'
4
4
  import { tap, spec } from 'node:test/reporters'
5
- import { run } from 'node:test'
6
- import { glob } from 'glob'
7
- import { findUp } from 'find-up'
8
- import { createRequire } from 'node:module'
9
- import { resolve, join, dirname } from 'node:path'
10
- import { access, readFile } from 'node:fs/promises'
11
- import { execa } from 'execa'
12
-
13
- async function isFileAccessible (filename, directory) {
14
- try {
15
- const filePath = directory ? resolve(directory, filename) : filename
16
- await access(filePath)
17
- return true
18
- } catch (err) {
19
- return false
20
- }
21
- }
5
+ import { mkdtemp, rm } from 'node:fs/promises'
6
+ import { finished } from 'node:stream/promises'
7
+ import { join, relative } from 'node:path'
8
+ import posix from 'node:path/posix'
9
+ import runWithTypeScript from './lib/run.js'
10
+ import { Report } from 'c8'
22
11
 
23
12
  let reporter
13
+ /* c8 ignore next 4 */
24
14
  if (process.stdout.isTTY) {
25
- reporter = spec()
15
+ /* eslint new-cap: "off" */
16
+ reporter = new spec()
26
17
  } else {
27
18
  reporter = tap
28
19
  }
@@ -33,7 +24,10 @@ const args = parseArgs({
33
24
  only: { type: 'boolean', short: 'o' },
34
25
  watch: { type: 'boolean', short: 'w' },
35
26
  pattern: { type: 'string', short: 'p' },
36
- concurrency: { type: 'string', short: 'c' }
27
+ concurrency: { type: 'string', short: 'c' },
28
+ coverage: { type: 'boolean', short: 'C' },
29
+ timeout: { type: 'string', short: 't', default: '30000' },
30
+ 'coverage-exclude': { type: 'string', short: 'X' }
37
31
  },
38
32
  allowPositionals: true
39
33
  })
@@ -42,56 +36,57 @@ if (args.values.concurrency) {
42
36
  args.values.concurrency = parseInt(args.values.concurrency)
43
37
  }
44
38
 
45
- const tsconfigPath = await findUp('tsconfig.json')
46
-
47
- let prefix = ''
48
-
49
- if (tsconfigPath) {
50
- try {
51
- const _require = createRequire(process.cwd())
52
- const typescriptPathCWD = _require.resolve('typescript')
53
- const tscPath = join(typescriptPathCWD, '..', '..', 'bin', 'tsc')
54
- if (tscPath) {
55
- const isAccessible = await isFileAccessible(tscPath)
56
- if (isAccessible) {
57
- await execa(tscPath, { cwd: dirname(tsconfigPath), stdio: 'inherit' })
58
- }
59
- }
60
- const tsconfig = JSON.parse(await readFile(tsconfigPath))
61
- const outDir = tsconfig.compilerOptions.outDir
62
- if (outDir) {
63
- prefix = join(dirname(tsconfigPath), outDir)
64
- }
65
- } catch (err) {
66
- console.log(err)
67
- }
39
+ if (args.values.timeout) {
40
+ args.values.timeout = parseInt(args.values.timeout)
68
41
  }
69
42
 
70
- let files
71
- if (args.positionals.length > 0) {
72
- if (prefix) {
73
- files = args.positionals.map((file) => join(prefix, file.replace(/ts$/, 'js')))
74
- } else {
75
- files = args.positionals
76
- }
77
- } else if (args.values.pattern) {
78
- if (prefix) {
79
- args.values.pattern = join(prefix, args.values.pattern)
80
- }
81
- files = await glob(args.values.pattern, { ignore: 'node_modules/**' })
82
- } else {
83
- if (prefix) {
84
- files = await glob(join(prefix, 'test/**/*.test.{cjs,mjs,js}'), { ignore: 'node_modules/**' })
85
- } else {
86
- files = await glob('test/**/*.test.{cjs,mjs,js}', { ignore: 'node_modules/**' })
87
- }
43
+ let covDir
44
+ if (args.values.coverage) {
45
+ covDir = await mkdtemp(join(process.cwd(), 'coverage-'))
46
+ process.env.NODE_V8_COVERAGE = covDir
88
47
  }
89
48
 
90
49
  const config = {
91
50
  ...args.values,
92
- files
51
+ files: args.positionals,
52
+ pattern: args.values.pattern,
53
+ cwd: process.cwd()
93
54
  }
94
55
 
95
- run(config)
96
- .compose(reporter)
97
- .pipe(process.stdout)
56
+ try {
57
+ const stream = await runWithTypeScript(config)
58
+
59
+ stream.on('test:fail', () => {
60
+ process.exitCode = 1
61
+ })
62
+
63
+ stream.compose(reporter).pipe(process.stdout)
64
+
65
+ await finished(stream)
66
+
67
+ if (covDir) {
68
+ let exclude = (args.values['coverage-exclude'] || '').split(',').filter(Boolean)
69
+
70
+ if (exclude.length === 0) {
71
+ exclude = undefined
72
+ } else if (config.prefix) {
73
+ const localPrefix = relative(process.cwd(), config.prefix)
74
+ exclude = exclude.map((file) => posix.join(localPrefix, file))
75
+ }
76
+ console.log('>> Excluding from coverage:', exclude)
77
+ const report = Report({
78
+ reporter: ['text'],
79
+ tempDirectory: covDir,
80
+ exclude
81
+ })
82
+
83
+ await report.run()
84
+ }
85
+ /* c8 ignore next 3 */
86
+ } catch (err) {
87
+ console.error(err)
88
+ } finally {
89
+ if (covDir) {
90
+ await rm(covDir, { recursive: true })
91
+ }
92
+ }
@@ -0,0 +1,6 @@
1
+ import { strictEqual } from 'node:assert'
2
+ import { test } from 'node:test'
3
+
4
+ test('this will fail', () => {
5
+ strictEqual(1, 2)
6
+ })
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,7 @@
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
+ })
@@ -18,6 +18,7 @@
18
18
  "skipLibCheck": true,
19
19
  "lib": [
20
20
  "ESNext"
21
- ]
21
+ ],
22
+ "incremental": true
22
23
  }
23
24
  }
@@ -0,0 +1,4 @@
1
+
2
+ export function add (x: number, y: number): number {
3
+ return x + y
4
+ }
@@ -0,0 +1,7 @@
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
+ })
@@ -0,0 +1,24 @@
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
+ }
package/lib/run.js ADDED
@@ -0,0 +1,144 @@
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 { resolve, join, dirname } from 'node:path'
6
+ import { access, readFile } from 'node:fs/promises'
7
+ import { execa } from 'execa'
8
+
9
+ async function isFileAccessible (filename, directory) {
10
+ try {
11
+ const filePath = directory ? resolve(directory, filename) : filename
12
+ await access(filePath)
13
+ return true
14
+ } catch (err) {
15
+ return false
16
+ }
17
+ }
18
+
19
+ function deferred () {
20
+ let resolve
21
+ let reject
22
+ const promise = new Promise((_resolve, _reject) => {
23
+ resolve = _resolve
24
+ reject = _reject
25
+ })
26
+ return { resolve, reject, promise }
27
+ }
28
+
29
+ export default async function runWithTypeScript (config) {
30
+ const { cwd } = config
31
+ const chunks = []
32
+ const tsconfigPath = await findUp('tsconfig.json', { cwd })
33
+
34
+ let prefix = ''
35
+ let tscPath
36
+
37
+ if (tsconfigPath) {
38
+ const _require = createRequire(cwd)
39
+ const typescriptPathCWD = _require.resolve('typescript')
40
+ tscPath = join(typescriptPathCWD, '..', '..', 'bin', 'tsc')
41
+ if (tscPath) {
42
+ const isAccessible = await isFileAccessible(tscPath)
43
+ if (isAccessible) {
44
+ // Watch is handled aftterwards
45
+ if (!config.watch) {
46
+ const start = Date.now()
47
+ await execa('node', [tscPath], { cwd: dirname(tsconfigPath) })
48
+ chunks.push({
49
+ type: 'test:diagnostic',
50
+ data: {
51
+ nesting: 0,
52
+ message: `TypeScript compilation complete (${Date.now() - start}ms)`
53
+ }
54
+ })
55
+ }
56
+ } else {
57
+ throw new Error('Could not find tsc')
58
+ }
59
+ }
60
+ const tsconfig = JSON.parse(await readFile(tsconfigPath))
61
+ const outDir = tsconfig.compilerOptions.outDir
62
+ if (outDir) {
63
+ prefix = join(dirname(tsconfigPath), outDir)
64
+ }
65
+ }
66
+ config.prefix = prefix
67
+ config.setup = (test) => {
68
+ for (const chunk of chunks) {
69
+ test.reporter.push(chunk)
70
+ }
71
+ }
72
+
73
+ let tscChild
74
+ /* eslint prefer-const: "off" */
75
+ let stream
76
+ let p
77
+
78
+ if (config.watch) {
79
+ p = deferred()
80
+ const start = Date.now()
81
+ tscChild = execa('node', [tscPath, '--watch'], { cwd })
82
+ tscChild.stdout.setEncoding('utf8')
83
+ tscChild.stdout.on('data', (data) => {
84
+ if (data.includes('Watching for file changes')) {
85
+ chunks.push({
86
+ type: 'test:diagnostic',
87
+ data: {
88
+ nesting: 0,
89
+ message: `TypeScript compilation complete (${Date.now() - start}ms)`
90
+ }
91
+ })
92
+
93
+ p.resolve()
94
+ }
95
+ if (data.includes('error TS')) {
96
+ const toPush = stream || chunks
97
+ toPush.push({
98
+ type: 'test:fail',
99
+ data: {
100
+ nesting: 0,
101
+ name: data.trim()
102
+ }
103
+ })
104
+ }
105
+ })
106
+ if (config.signal) {
107
+ config.signal.addEventListener('abort', () => {
108
+ tscChild.kill()
109
+ })
110
+ }
111
+ }
112
+
113
+ if (p) {
114
+ await p.promise
115
+ }
116
+
117
+ let files = config.files || []
118
+ const ignore = join('node_modules', '**')
119
+ if (files.length > 0) {
120
+ if (prefix) {
121
+ files = files.map((file) => join(prefix, file.replace(/ts$/, 'js')))
122
+ }
123
+ } else if (config.pattern) {
124
+ if (prefix) {
125
+ config.pattern = join(prefix, config.pattern)
126
+ }
127
+ files = await glob(config.pattern, { ignore, cwd, windowsPathsNoEscape: true })
128
+ } else if (prefix) {
129
+ files = await glob(join(prefix, join('test', '**', '*.test.{cjs,mjs,js}')), { ignore, cwd, windowsPathsNoEscape: true })
130
+ } else {
131
+ files = await glob(join('test', '**', '*.test.{cjs,mjs,js}'), { ignore, cwd, windowsPathsNoEscape: true })
132
+ }
133
+
134
+ config.files = files
135
+
136
+ stream = run(config)
137
+
138
+ stream.on('close', () => {
139
+ if (tscChild) {
140
+ tscChild.kill()
141
+ }
142
+ })
143
+ return stream
144
+ }
package/package.json CHANGED
@@ -1,22 +1,28 @@
1
1
  {
2
2
  "name": "borp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "node:test wrapper with TypeScript support",
6
6
  "main": "borp.js",
7
7
  "scripts": {
8
- "test": "standard | snazzy"
8
+ "clean": "rm -rf fixtures/*/dist .test-* coverage-*",
9
+ "lint": "standard | snazzy",
10
+ "unit": "node borp.js --concurrency=1 --coverage --coverage-exclude \"fixtures/**/*,test/**/*\"",
11
+ "test": "npm run clean ; npm run lint && npm run unit"
9
12
  },
10
13
  "keywords": [],
11
14
  "author": "Matteo Collina <hello@matteocollina.com>",
12
15
  "license": "MIT",
13
16
  "devDependencies": {
17
+ "@matteo.collina/tspl": "^0.1.0",
14
18
  "@types/node": "^20.10.0",
19
+ "desm": "^1.3.0",
15
20
  "snazzy": "^9.0.0",
16
21
  "standard": "^17.1.0",
17
22
  "typescript": "^5.3.2"
18
23
  },
19
24
  "dependencies": {
25
+ "c8": "^8.0.1",
20
26
  "execa": "^8.0.1",
21
27
  "find-up": "^7.0.0",
22
28
  "glob": "^10.3.10"
@@ -0,0 +1,47 @@
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
+
6
+ test('ts-esm', async (t) => {
7
+ const { strictEqual, completed, match } = tspl(t, { plan: 4 })
8
+ const config = {
9
+ files: [],
10
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
11
+ }
12
+
13
+ const stream = await runWithTypeScript(config)
14
+
15
+ const names = new Set(['add', 'add2'])
16
+
17
+ stream.once('data', (test) => {
18
+ strictEqual(test.type, 'test:diagnostic')
19
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
20
+ })
21
+
22
+ stream.on('test:pass', (test) => {
23
+ strictEqual(names.has(test.name), true)
24
+ names.delete(test.name)
25
+ })
26
+
27
+ await completed
28
+ })
29
+
30
+ test('ts-cjs', async (t) => {
31
+ const { strictEqual, completed } = tspl(t, { plan: 2 })
32
+ const config = {
33
+ files: [],
34
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-cjs')
35
+ }
36
+
37
+ const stream = await runWithTypeScript(config)
38
+
39
+ const names = new Set(['add', 'add2'])
40
+
41
+ stream.on('test:pass', (test) => {
42
+ strictEqual(names.has(test.name), true)
43
+ names.delete(test.name)
44
+ })
45
+
46
+ await completed
47
+ })
@@ -0,0 +1,25 @@
1
+ import { test } from 'node:test'
2
+ import { execa } from 'execa'
3
+ import { join } from 'desm'
4
+ import { rejects } from 'node:assert'
5
+
6
+ const borp = join(import.meta.url, '..', 'borp.js')
7
+
8
+ test('limit concurrency', async () => {
9
+ await execa('node', [
10
+ borp,
11
+ '--concurrency',
12
+ '1'
13
+ ], {
14
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
15
+ })
16
+ })
17
+
18
+ test('failing test set correct status code', async () => {
19
+ // execa rejects if status code is not 0
20
+ await rejects(execa('node', [
21
+ borp
22
+ ], {
23
+ cwd: join(import.meta.url, '..', 'fixtures', 'fails')
24
+ }))
25
+ })
@@ -0,0 +1,36 @@
1
+ import { test } from 'node:test'
2
+ import { match, doesNotMatch } from 'node:assert'
3
+ import { execa } from 'execa'
4
+ import { join } from 'desm'
5
+
6
+ const borp = join(import.meta.url, '..', 'borp.js')
7
+
8
+ test('coverage', async () => {
9
+ const res = await execa('node', [
10
+ borp,
11
+ '--coverage'
12
+ ], {
13
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
14
+ })
15
+
16
+ match(res.stdout, /% Stmts/)
17
+ match(res.stdout, /All files/)
18
+ match(res.stdout, /add\.ts/)
19
+ })
20
+
21
+ test('coverage excludes', async () => {
22
+ const res = await execa('node', [
23
+ borp,
24
+ '--coverage',
25
+ '--coverage-exclude=src'
26
+ ], {
27
+ cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
28
+ })
29
+
30
+ match(res.stdout, /% Stmts/)
31
+ match(res.stdout, /All files/)
32
+ doesNotMatch(res.stdout, /add\.ts/)
33
+ // The test files are shown
34
+ match(res.stdout, /add\.test\.ts/)
35
+ match(res.stdout, /add2\.test\.ts/)
36
+ })
@@ -0,0 +1,107 @@
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
+
9
+ test('watch', async (t) => {
10
+ const { strictEqual, completed, match } = tspl(t, { plan: 3 })
11
+
12
+ const dir = path.resolve(await mkdtemp('.test-watch'))
13
+ await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm'), dir, {
14
+ recursive: true
15
+ })
16
+
17
+ const controller = new AbortController()
18
+ t.after(async () => {
19
+ controller.abort()
20
+ await rm(dir, { recursive: true })
21
+ })
22
+
23
+ const config = {
24
+ files: [],
25
+ cwd: dir,
26
+ signal: controller.signal,
27
+ watch: true
28
+ }
29
+
30
+ const stream = await runWithTypeScript(config)
31
+
32
+ const fn = (test) => {
33
+ if (test.type === 'test:fail') {
34
+ strictEqual(test.data.name, 'add')
35
+ stream.removeListener('data', fn)
36
+ }
37
+ }
38
+ stream.on('data', fn)
39
+
40
+ const [test] = await once(stream, 'data')
41
+ strictEqual(test.type, 'test:diagnostic')
42
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
43
+
44
+ const toWrite = `
45
+ import { test } from 'node:test'
46
+ import { add } from '../src/add.js'
47
+ import { strictEqual } from 'node:assert'
48
+
49
+ test('add', () => {
50
+ strictEqual(add(1, 2), 4)
51
+ })
52
+ `
53
+ const file = path.join(dir, 'test', 'add.test.ts')
54
+ await writeFile(file, toWrite)
55
+
56
+ await completed
57
+ })
58
+
59
+ test('watch file syntax error', async (t) => {
60
+ const { strictEqual, completed, match } = tspl(t, { plan: 3 })
61
+
62
+ const dir = path.resolve(await mkdtemp('.test-watch'))
63
+ await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm'), dir, {
64
+ recursive: true
65
+ })
66
+
67
+ const controller = new AbortController()
68
+ t.after(async () => {
69
+ controller.abort()
70
+ await rm(dir, { recursive: true })
71
+ })
72
+
73
+ const config = {
74
+ files: [],
75
+ cwd: dir,
76
+ signal: controller.signal,
77
+ watch: true
78
+ }
79
+
80
+ const stream = await runWithTypeScript(config)
81
+
82
+ const fn = (test) => {
83
+ if (test.type === 'test:fail') {
84
+ match(test.data.name, /add\.test\.ts/)
85
+ stream.removeListener('data', fn)
86
+ }
87
+ }
88
+ stream.on('data', fn)
89
+
90
+ const [test] = await once(stream, 'data')
91
+ strictEqual(test.type, 'test:diagnostic')
92
+ match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
93
+
94
+ const toWrite = `
95
+ import { test } from 'node:test'
96
+ import { add } from '../src/add.js'
97
+ import { strictEqual } from 'node:assert'
98
+
99
+ test('add', () => {
100
+ strictEqual(add(1, 2), 3
101
+ })
102
+ `
103
+ const file = path.join(dir, 'test', 'add.test.ts')
104
+ await writeFile(file, toWrite)
105
+
106
+ await completed
107
+ })
File without changes
File without changes