1
0
Fork 0
mirror of https://github.com/dorny/test-reporter.git synced 2026-02-04 05:27:55 +01:00

feat: Add listTestCaseTime flag to print test times next to test names

- Add a new listTestCaseTime flag, that optionally prints test times
next to test names. This flag defaults to false, for backward
compatibility.
- Update get-report.ts to use this flag when generating a report.
- Update existing tests to set ReportOptions listTestCaseTime: true to
verify above feature.
- Update README with documentation about this new flag.

Note this feature was needed for individual test times under pytest,
since the xml is generated with all tests under one test suite.

https://github.com/dorny/test-reporter/issues/260
This commit is contained in:
Rick Shanor 2026-01-06 07:39:38 -08:00
parent a810f9bf83
commit fb2dd2ba55
5 changed files with 31 additions and 7 deletions

View file

@ -184,6 +184,10 @@ jobs:
# none # none
list-tests: 'all' list-tests: 'all'
# Show execution time for individual test cases
# When enabled, test case execution times will be displayed in parentheses next to test names
list-test-case-time: 'false'
# Limits number of created annotations with error message and stack trace captured during test execution. # Limits number of created annotations with error message and stack trace captured during test execution.
# Must be less or equal to 50. # Must be less or equal to 50.
max-annotations: '10' max-annotations: '10'

View file

@ -11,9 +11,9 @@
### ❌ <a id="user-content-r0s0" href="#user-content-r0s0">__tests__\main.test.js</a> ### ❌ <a id="user-content-r0s0" href="#user-content-r0s0">__tests__\main.test.js</a>
``` ```
Test 1 Test 1
✅ Passing test ✅ Passing test (1ms)
Test 1 Test 1.1 Test 1 Test 1.1
❌ Failing test ❌ Failing test (2ms)
Error: expect(received).toBeTruthy() Error: expect(received).toBeTruthy()
❌ Exception in target unit ❌ Exception in target unit
Error: Some error Error: Some error
@ -23,7 +23,7 @@ Test 2
``` ```
### ❌ <a id="user-content-r0s1" href="#user-content-r0s1">__tests__\second.test.js</a> ### ❌ <a id="user-content-r0s1" href="#user-content-r0s1">__tests__\second.test.js</a>
``` ```
❌ Timeout test ❌ Timeout test (4ms)
: Timeout - Async callback was not invoked within the 1 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 1 ms timeout specified by jest.setTimeout.Error: : Timeout - Async callback was not invoked within the 1 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 1 ms timeout specified by jest.setTimeout.Error:
⚪ Skipped test ⚪ Skipped test
``` ```

View file

@ -3,7 +3,7 @@ import * as path from 'path'
import {JestJunitParser} from '../src/parsers/jest-junit/jest-junit-parser' import {JestJunitParser} from '../src/parsers/jest-junit/jest-junit-parser'
import {ParseOptions} from '../src/test-parser' import {ParseOptions} from '../src/test-parser'
import {DEFAULT_OPTIONS, getReport} from '../src/report/get-report' import {DEFAULT_OPTIONS, getReport, ReportOptions} from '../src/report/get-report'
import {normalizeFilePath} from '../src/utils/path-utils' import {normalizeFilePath} from '../src/utils/path-utils'
describe('jest-junit tests', () => { describe('jest-junit tests', () => {
@ -55,7 +55,12 @@ describe('jest-junit tests', () => {
const result = await parser.parse(filePath, fileContent) const result = await parser.parse(filePath, fileContent)
expect(result).toMatchSnapshot() expect(result).toMatchSnapshot()
const report = getReport([result]) const reportOpts: ReportOptions = {
...DEFAULT_OPTIONS,
listTestCaseTime: true
}
const report = getReport([result], reportOpts)
fs.mkdirSync(path.dirname(outputPath), {recursive: true}) fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, report) fs.writeFileSync(outputPath, report)
}) })

View file

@ -43,6 +43,7 @@ class TestReporter {
readonly reporter = core.getInput('reporter', {required: true}) readonly reporter = core.getInput('reporter', {required: true})
readonly listSuites = core.getInput('list-suites', {required: true}) as 'all' | 'failed' | 'none' readonly listSuites = core.getInput('list-suites', {required: true}) as 'all' | 'failed' | 'none'
readonly listTests = core.getInput('list-tests', {required: true}) as 'all' | 'failed' | 'none' readonly listTests = core.getInput('list-tests', {required: true}) as 'all' | 'failed' | 'none'
readonly listTestCaseTime = core.getInput('list-test-case-time', {required: false}) === 'true'
readonly maxAnnotations = parseInt(core.getInput('max-annotations', {required: true})) readonly maxAnnotations = parseInt(core.getInput('max-annotations', {required: true}))
readonly failOnError = core.getInput('fail-on-error', {required: true}) === 'true' readonly failOnError = core.getInput('fail-on-error', {required: true}) === 'true'
readonly failOnEmpty = core.getInput('fail-on-empty', {required: true}) === 'true' readonly failOnEmpty = core.getInput('fail-on-empty', {required: true}) === 'true'
@ -174,7 +175,16 @@ class TestReporter {
} }
} }
const {listSuites, listTests, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed} = this const {
listSuites,
listTests,
listTestCaseTime,
onlySummary,
useActionsSummary,
badgeTitle,
reportTitle,
collapsed
} = this
const passed = results.reduce((sum, tr) => sum + tr.passed, 0) const passed = results.reduce((sum, tr) => sum + tr.passed, 0)
const failed = results.reduce((sum, tr) => sum + tr.failed, 0) const failed = results.reduce((sum, tr) => sum + tr.failed, 0)
@ -188,6 +198,7 @@ class TestReporter {
{ {
listSuites, listSuites,
listTests, listTests,
listTestCaseTime,
baseUrl, baseUrl,
onlySummary, onlySummary,
useActionsSummary, useActionsSummary,
@ -219,6 +230,7 @@ class TestReporter {
const summary = getReport(results, { const summary = getReport(results, {
listSuites, listSuites,
listTests, listTests,
listTestCaseTime,
baseUrl, baseUrl,
onlySummary, onlySummary,
useActionsSummary, useActionsSummary,

View file

@ -11,6 +11,7 @@ const MAX_ACTIONS_SUMMARY_LENGTH = 1048576
export interface ReportOptions { export interface ReportOptions {
listSuites: 'all' | 'failed' | 'none' listSuites: 'all' | 'failed' | 'none'
listTests: 'all' | 'failed' | 'none' listTests: 'all' | 'failed' | 'none'
listTestCaseTime: boolean
baseUrl: string baseUrl: string
onlySummary: boolean onlySummary: boolean
useActionsSummary: boolean useActionsSummary: boolean
@ -22,6 +23,7 @@ export interface ReportOptions {
export const DEFAULT_OPTIONS: ReportOptions = { export const DEFAULT_OPTIONS: ReportOptions = {
listSuites: 'all', listSuites: 'all',
listTests: 'all', listTests: 'all',
listTestCaseTime: false,
baseUrl: '', baseUrl: '',
onlySummary: false, onlySummary: false,
useActionsSummary: true, useActionsSummary: true,
@ -281,7 +283,8 @@ function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: numbe
continue continue
} }
const result = getResultIcon(tc.result) const result = getResultIcon(tc.result)
sections.push(`${space}${result} ${tc.name}`) const time = options.listTestCaseTime && tc.time ? ` (${formatTime(tc.time)})` : ''
sections.push(`${space}${result} ${tc.name}${time}`)
if (tc.error) { if (tc.error) {
const lines = (tc.error.message ?? getFirstNonEmptyLine(tc.error.details)?.trim()) const lines = (tc.error.message ?? getFirstNonEmptyLine(tc.error.details)?.trim())
?.split(/\r?\n/g) ?.split(/\r?\n/g)