Skip to content
This repository was archived by the owner on Dec 28, 2024. It is now read-only.

Commit 8015a5e

Browse files
committed
feat: add TAP reporter
1 parent 444ce0e commit 8015a5e

File tree

10 files changed

+327
-48
lines changed

10 files changed

+327
-48
lines changed

lib/formatter.tap.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

lib/lib.node.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

lib/formatter.fancy.js renamed to lib/reporters/fancy.js

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
1-
/** @typedef {import("./fn.runTest").Diagnosis} Diagnosis */
1+
/** @typedef {import("./types").IntroFormatter} IntroFormatter */
2+
/** @typedef {import("./types").TestFormatter} TestFormatter */
3+
/** @typedef {import("./types").SuiteFormatter} SuiteFormatter */
4+
/** @typedef {import("./types").Reporter} Reporter */
25

36
import path from "node:path"
4-
import { red, green, black, gray, white, bold } from "./lib.colors.js"
5-
import { formatHrTime } from "./lib.node.js"
7+
8+
import { red, green, black, gray, white, bold } from "../utils/colors.js"
9+
import { formatHrTime } from "../utils/node.js"
610

711
const PASS_TAG = green.bgSecondary(black.fg(" PASS "))
812
const FAIL_TAG = red.bgSecondary(black.fg(" FAIL "))
913

14+
/**
15+
* Format the intro message similar to Jest's default reporter
16+
*
17+
* @type {IntroFormatter}
18+
*
19+
* @example
20+
* formatIntro({ count: 1 })
21+
* // => "Running 1 file through tsd-lite\n"
22+
*
23+
* @example
24+
* formatIntro({ count: 4 })
25+
* // => "Running 4 files through tsd-lite\n"
26+
*/
27+
const formatIntro = ({ count }) => {
28+
const files = count === 1 ? "file" : "files"
29+
30+
return bold(`Running ${count} ${files} through tsd-lite\n`)
31+
}
32+
1033
/**
1134
* Emphasize the file name by contrasting it with the rest of the path
1235
*
@@ -26,20 +49,19 @@ const highlightFileName = filePath => {
2649
}
2750

2851
/**
29-
* Format each test file result similar to Jest's default reporter
52+
* Format test file result similar to Jest's default reporter
3053
*
31-
* @param {Diagnosis} input
32-
* @returns {string}
54+
* @type {TestFormatter}
3355
*
3456
* @example
35-
* formatTestResult({
57+
* formatTest({
3658
* name: "src/__tests__/index.test.js",
3759
* errors: [],
3860
* })
3961
* // => " PASS src/__tests__/index.test.js 0.8s"
4062
*
4163
* @example
42-
* formatTestResult({
64+
* formatTest({
4365
* name: "src/__tests__/index.test.js",
4466
* errors: [
4567
* { row: 1, column: 1, message: "Unexpected token" },
@@ -49,7 +71,7 @@ const highlightFileName = filePath => {
4971
* // => " (1:1) Unexpected token
5072
* // => ""
5173
*/
52-
export const formatTestResult = ({ name, duration, errors = [] }) => {
74+
const formatTest = ({ result: { name, duration, errors = [] } }) => {
5375
const isPass = errors.length === 0
5476
const friendlyDuration = formatHrTime(duration)
5577
const title = `${isPass ? PASS_TAG : FAIL_TAG} ${highlightFileName(
@@ -70,23 +92,20 @@ export const formatTestResult = ({ name, duration, errors = [] }) => {
7092
}
7193

7294
/**
73-
* Format the summary of multiple test files results
95+
* Format the summary of multiple tests similar to Jest's default reporter
7496
*
75-
* @param {Object} props
76-
* @param {number} props.passCount
77-
* @param {number} props.failCount
78-
* @param {[number, number]} props.duration
97+
* @type {SuiteFormatter}
7998
*
8099
* @example
81-
* formatSuiteResult({
100+
* formatSuite({
82101
* passCount: 1,
83102
* failCount: 1,
84103
* duration: [5, 420000000]
85104
* })
86105
* // => "Summary: 1 failed, 1 passed, 2 total"
87106
* // => "Duration: 5.42s"
88107
*/
89-
export const formatSuiteResult = ({ passCount, failCount, duration }) => {
108+
const formatSuite = ({ passCount, failCount, duration }) => {
90109
const filesFail = red.fgSecondary(`${failCount} failed`)
91110
const filesPass = green.fgSecondary(`${passCount} passed`)
92111
const filesTotal = `${failCount + passCount} total`
@@ -100,3 +119,10 @@ export const formatSuiteResult = ({ passCount, failCount, duration }) => {
100119
`${durationLabel} ${formatHrTime(duration)}`,
101120
].join("\n")
102121
}
122+
123+
/** @type {Reporter} */
124+
export default {
125+
formatIntro,
126+
formatTest,
127+
formatSuite,
128+
}

lib/reporters/tap.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/** @typedef {import("./types").IntroFormatter} IntroFormatter */
2+
/** @typedef {import("./types").TestFormatter} TestFormatter */
3+
/** @typedef {import("./types").SuiteFormatter} SuiteFormatter */
4+
/** @typedef {import("./types").Reporter} Reporter */
5+
6+
import { formatHrTime } from "../utils/node.js"
7+
8+
/**
9+
* Format intro to TAP format
10+
*
11+
* @type {IntroFormatter}
12+
*
13+
* @example
14+
* formatIntro({ count: 2 })
15+
* // => "TAP version 13"
16+
* // => "1..2"
17+
*/
18+
const formatIntro = ({ count, description }) =>
19+
["TAP version 14", `1..${count}`, description ? `# ${description}` : ""].join(
20+
"\n"
21+
)
22+
23+
/**
24+
* Format test file result to TAP format
25+
*
26+
* @type {TestFormatter}
27+
*
28+
* @example
29+
* formatTestResult({
30+
* name: "src/__tests__/index.test.js",
31+
* errors: [],
32+
* })
33+
* // => "ok 1 - src/__tests__/index.test.js 0.8s"
34+
*
35+
* @example
36+
* formatTestResult({
37+
* name: "src/__tests__/index.test.js",
38+
* errors: [
39+
* { row: 1, column: 1, message: "Unexpected token" },
40+
* ],
41+
* })
42+
* // => "not ok 1 - src/__tests__/index.test.js 1.2s"
43+
* // => " ---"
44+
* // => " message: 'Unexpected token'
45+
* // => " severity: fail"
46+
* // => " data:"
47+
* // => " row: 1"
48+
* // => " column: 1"
49+
* // => " ..."
50+
*/
51+
const formatTest = ({ index, result: { name, duration, errors } }) => {
52+
const humanReadableDuration = formatHrTime(duration)
53+
54+
if (errors.length === 0) {
55+
return `ok ${index} - ${name} ${humanReadableDuration}`
56+
}
57+
58+
const output = [`not ok - ${index} ${name} ${humanReadableDuration}`]
59+
60+
for (const error of errors) {
61+
output.push(
62+
` ---`,
63+
` message: '${error.message}'`,
64+
` severity: fail`,
65+
` data:`,
66+
` row: ${error.row}`,
67+
` column: ${error.column}`,
68+
` ...`
69+
)
70+
}
71+
72+
return output.join("\n")
73+
}
74+
75+
/**
76+
* Format the summary of multiple tests to TAP format
77+
*
78+
* @type {SuiteFormatter}
79+
*
80+
* @example
81+
* formatSuiteResult({
82+
* passCount: 1,
83+
* failCount: 1,
84+
* duration: [5, 420000000]
85+
* })
86+
* // => "1..2"
87+
* // => "# tests 2"
88+
* // => "# pass 1"
89+
* // => "# fail 1"
90+
* // => "Duration: 5.42s"
91+
*/
92+
const formatSuite = ({ passCount, failCount, duration }) => {
93+
const totalCount = passCount + failCount
94+
95+
return [
96+
`# tests ${totalCount}`,
97+
`# pass ${passCount}`,
98+
`# fail ${failCount}`,
99+
`# time ${formatHrTime(duration)}`,
100+
].join("\n")
101+
}
102+
103+
/** @type {Reporter} */
104+
export default {
105+
formatIntro,
106+
formatTest,
107+
formatSuite,
108+
}

lib/reporters/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { TestResult } from "../runTest.js"
2+
3+
export type IntroFormatter = (props: {
4+
count: number
5+
description?: string
6+
}) => string
7+
8+
export type TestFormatter = (props: {
9+
index?: number
10+
result: TestResult
11+
}) => string
12+
13+
export type SuiteFormatter = (props: {
14+
passCount: number
15+
failCount: number
16+
duration: [number, number]
17+
}) => string
18+
19+
export type Reporter = {
20+
formatIntro: IntroFormatter
21+
formatTest: TestFormatter
22+
formatSuite: SuiteFormatter
23+
}

lib/runSuite.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { runTest } from "./runTest.js"
2+
3+
/**
4+
* @callback OnTestFinish
5+
* @param {ReturnType<runTest>} result
6+
* @param {number} index
7+
* @returns {void}
8+
*/
9+
10+
/**
11+
* @callback OnSuiteFinish
12+
* @param {ReturnType<runTest>[]} results
13+
* @returns {void}
14+
*/
15+
16+
/**
17+
* Run a suite of test files and return a more friendly result format
18+
*
19+
* @param {string[]} absolutePaths
20+
* @param {Object} props
21+
* @param {OnTestFinish} props.onTestFinish
22+
* @param {OnSuiteFinish} props.onSuiteFinish
23+
* @returns {void}
24+
*
25+
* @example
26+
* runSuite(["/home/lorem/src/fn.test-d.ts"], {
27+
* onTestFinish: (result, index) => {},
28+
* onSuiteDone: (results) => {},
29+
* })
30+
*/
31+
export const runSuite = (absolutePaths, { onTestFinish, onSuiteFinish }) => {
32+
const results = absolutePaths.map((item, index) => {
33+
// TODO: async via worker threads maybe?
34+
const result = runTest(item)
35+
36+
onTestFinish(result, index)
37+
38+
return result
39+
})
40+
41+
onSuiteFinish(results)
42+
}

lib/fn.runTest.js renamed to lib/runTest.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,47 @@ import tsdLite from "tsd-lite"
55
const tsd = /** @type {typeof tsdLite} */ (tsdLite.default)
66

77
/**
8-
* @typedef DiagnosisError
9-
*
8+
* @typedef TestError
109
* @property {string} message
1110
* @property {number} [row]
1211
* @property {number} [column]
1312
*/
1413

1514
/**
16-
* @typedef Diagnosis
17-
*
15+
* @typedef TestResult
1816
* @property {string} name
1917
* @property {number} assertionCount
2018
* @property {[number, number]} duration
21-
* @property {DiagnosisError[]} errors
19+
* @property {TestError[]} errors
2220
*/
2321

2422
/**
23+
* Run a test file and return a more friendly result format
24+
*
2525
* @param {string} absolutePath
26-
* @returns {Diagnosis}
26+
* @returns {TestResult}
2727
*
2828
* @example
2929
* runTest("/home/lorem/src/fn.test-d.ts")
3030
* // {
3131
* // name: "src/fn.test-d.ts",
32-
* // pass: true,
3332
* // assertionCount: 1,
33+
* // duration: [1, 123456789],
3434
* // errors: []
3535
* // }
36+
*
37+
* @example
38+
* runTest("/home/lorem/src/fn-with-error.test-d.ts")
39+
* // {
40+
* // name: "src/fn.test-d.ts",
41+
* // assertionCount: 1,
42+
* // duration: [1, 123456789],
43+
* // errors: [{
44+
* // message: "Argument of type 'string' is not assignable ...",
45+
* // row: 1,
46+
* // column: 1,
47+
* // }]
48+
* // }
3649
*/
3750
export const runTest = absolutePath => {
3851
const startAt = process.hrtime()

lib/lib.colors.js renamed to lib/utils/colors.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,10 @@ export const black = {
5151
fg: input => `\u001B[30m${input}\u001B[0m`,
5252
}
5353

54+
export const cyan = {
55+
/** @param {string} input */
56+
fg: input => `\u001B[36m${input}\u001B[0m`,
57+
}
58+
5459
/** @param {string} input */
5560
export const bold = input => `\u001B[1m${input}\u001B[0m`

0 commit comments

Comments
 (0)