diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a8c2b..3b107e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 3.1.0 +* Feature: Add `list-files` input to control test report file listing https://github.com/dorny/test-reporter/pull/773 + ## 3.0.0 * Feature: Use NodeJS 24 LTS as default runtime https://github.com/dorny/test-reporter/pull/738 diff --git a/README.md b/README.md index 38b2285..81d04a5 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,12 @@ jobs: # none list-tests: 'all' + # Limits which test result files are listed: + # all + # failed + # none + list-files: 'all' + # Limits number of created annotations with error message and stack trace captured during test execution. # Must be less or equal to 50. max-annotations: '10' diff --git a/__tests__/report/get-report.test.ts b/__tests__/report/get-report.test.ts index 30e1e89..560f849 100644 --- a/__tests__/report/get-report.test.ts +++ b/__tests__/report/get-report.test.ts @@ -1,4 +1,5 @@ -import {getBadge, DEFAULT_OPTIONS, ReportOptions} from '../../src/report/get-report.js' +import {DEFAULT_OPTIONS, getBadge, getReport, ReportOptions} from '../../src/report/get-report.js' +import {TestCaseResult, TestGroupResult, TestRunResult, TestSuiteResult} from '../../src/test-results.js' describe('getBadge', () => { describe('URI encoding with special characters', () => { @@ -131,3 +132,147 @@ describe('getBadge', () => { }) }) }) + +describe('getReport', () => { + // Helper function to create test results + function createTestResult(path: string, passed: number, failed: number, skipped: number): TestRunResult { + const tests: TestCaseResult[] = [] + for (let i = 0; i < passed; i++) { + tests.push(new TestCaseResult(`passed-test-${i}`, 'success', 100)) + } + for (let i = 0; i < failed; i++) { + tests.push( + new TestCaseResult(`failed-test-${i}`, 'failed', 100, { + details: 'Test failed', + message: 'Assertion error' + }) + ) + } + for (let i = 0; i < skipped; i++) { + tests.push(new TestCaseResult(`skipped-test-${i}`, 'skipped', 0)) + } + + const group = new TestGroupResult('test-group', tests) + const suite = new TestSuiteResult('test-suite', [group]) + return new TestRunResult(path, [suite]) + } + + describe('list-files parameter', () => { + const results = [ + createTestResult('passing-file.spec.ts', 5, 0, 0), + createTestResult('failing-file.spec.ts', 3, 2, 1), + createTestResult('passing-with-skipped-file.spec.ts', 8, 0, 2) + ] + + it('shows all files when list-files is "all"', () => { + const report = getReport(results, { + ...DEFAULT_OPTIONS, + listFiles: 'all', + listSuites: 'none', + listTests: 'none' + }) + + expect(report).toContain('|Report|Passed|Failed|Skipped|Time|') + expect(report).toContain('passing-file.spec.ts') + expect(report).toContain('failing-file.spec.ts') + expect(report).toContain('passing-with-skipped-file.spec.ts') + }) + + it('shows only failed files when list-files is "failed"', () => { + const report = getReport(results, { + ...DEFAULT_OPTIONS, + listFiles: 'failed', + listSuites: 'none', + listTests: 'none' + }) + + expect(report).toContain('|Report|Passed|Failed|Skipped|Time|') + expect(report).not.toContain('passing-file.spec.ts') + expect(report).toContain('failing-file.spec.ts') + expect(report).not.toContain('passing-with-skipped-file.spec.ts') + }) + + it('shows no file details when list-files is "none"', () => { + const report = getReport(results, { + ...DEFAULT_OPTIONS, + listFiles: 'none', + listSuites: 'none', + listTests: 'none' + }) + + expect(report).toContain('![') + expect(report).not.toContain('|Report|Passed|Failed|Skipped|Time|') + expect(report).not.toContain('passing-file.spec.ts') + expect(report).not.toContain('failing-file.spec.ts') + expect(report).not.toContain('passing-with-skipped-file.spec.ts') + }) + + it('does not show an empty summary table when list-files is "none" and only-summary is enabled', () => { + const report = getReport(results, { + ...DEFAULT_OPTIONS, + listFiles: 'none', + listSuites: 'all', + onlySummary: true, + listTests: 'none' + }) + + expect(report).toContain('![') + expect(report).not.toContain('|Report|Passed|Failed|Skipped|Time|') + expect(report).not.toContain('passing-file.spec.ts') + expect(report).not.toContain('failing-file.spec.ts') + expect(report).not.toContain('passing-with-skipped-file.spec.ts') + }) + + it('works correctly with list-suites and list-tests when list-files is "failed"', () => { + const report = getReport(results, { + ...DEFAULT_OPTIONS, + listFiles: 'failed', + listSuites: 'all', + listTests: 'all' + }) + + expect(report).toContain('|Report|Passed|Failed|Skipped|Time|') + expect(report).not.toContain('passing-file.spec.ts') + expect(report).toContain('failing-file.spec.ts') + expect(report).not.toContain('passing-with-skipped-file.spec.ts') + // Should show suite details for the failed file + expect(report).toContain('test-suite') + }) + + it('filters correctly when all files pass and list-files is "failed"', () => { + const allPassingResults = [ + createTestResult('passing-file-1.spec.ts', 5, 0, 0), + createTestResult('passing-file-2.spec.ts', 8, 0, 2) + ] + + const report = getReport(allPassingResults, { + ...DEFAULT_OPTIONS, + listFiles: 'failed', + listSuites: 'all', + listTests: 'none' + }) + + expect(report).not.toContain('passing-file-1.spec.ts') + expect(report).not.toContain('passing-file-2.spec.ts') + expect(report).toContain('![') + expect(report).not.toContain('|Report|Passed|Failed|Skipped|Time|') + }) + + it('filters correctly when all files fail and list-files is "failed"', () => { + const allFailingResults = [ + createTestResult('failing-file-1.spec.ts', 0, 5, 0), + createTestResult('failing-file-2.spec.ts', 1, 2, 1) + ] + + const report = getReport(allFailingResults, { + ...DEFAULT_OPTIONS, + listFiles: 'failed', + listSuites: 'all', + listTests: 'none' + }) + + expect(report).toContain('failing-file-1.spec.ts') + expect(report).toContain('failing-file-2.spec.ts') + }) + }) +}) diff --git a/action.yml b/action.yml index be606d4..577b0c3 100644 --- a/action.yml +++ b/action.yml @@ -54,6 +54,14 @@ inputs: - none required: false default: 'all' + list-files: + description: | + Limits which test result files are listed. Supported options: + - all + - failed + - none + required: false + default: 'all' max-annotations: description: | Limits number of created annotations with error message and stack trace captured during test execution. diff --git a/dist/index.js b/dist/index.js index c8865b0..2847890 100644 --- a/dist/index.js +++ b/dist/index.js @@ -56954,6 +56954,7 @@ const DEFAULT_OPTIONS = { listSuites: 'all', listTests: 'all', slugPrefix: '', + listFiles: 'all', baseUrl: '', onlySummary: false, useActionsSummary: true, @@ -57074,25 +57075,31 @@ function getTestRunsReport(testRuns, options) { sections.push(`
Expand for details`); sections.push(` `); } - if (testRuns.length > 0 || options.onlySummary) { - const tableData = testRuns - .map((tr, originalIndex) => ({ tr, originalIndex })) - .filter(({ tr }) => tr.passed > 0 || tr.failed > 0 || tr.skipped > 0) - .map(({ tr, originalIndex }) => { - const time = formatTime(tr.time); - const name = tr.path; - const addr = options.baseUrl + makeRunSlug(originalIndex, options).link; - const nameLink = markdown_utils_link(name, addr); - const passed = tr.passed > 0 ? `${tr.passed} ${Icon.success}` : ''; - const failed = tr.failed > 0 ? `${tr.failed} ${Icon.fail}` : ''; - const skipped = tr.skipped > 0 ? `${tr.skipped} ${Icon.skip}` : ''; - return [nameLink, passed, failed, skipped, time]; - }); + // Filter test runs based on list-files option + const filteredTestRuns = options.listFiles === 'failed' + ? testRuns.filter(tr => tr.result === 'failed') + : options.listFiles === 'none' + ? [] + : testRuns; + const tableData = filteredTestRuns + .map((tr, originalIndex) => ({ tr, originalIndex })) + .filter(({ tr }) => tr.passed > 0 || tr.failed > 0 || tr.skipped > 0) + .map(({ tr, originalIndex }) => { + const time = formatTime(tr.time); + const name = tr.path; + const addr = options.baseUrl + makeRunSlug(originalIndex, options).link; + const nameLink = markdown_utils_link(name, addr); + const passed = tr.passed > 0 ? `${tr.passed} ${Icon.success}` : ''; + const failed = tr.failed > 0 ? `${tr.failed} ${Icon.fail}` : ''; + const skipped = tr.skipped > 0 ? `${tr.skipped} ${Icon.skip}` : ''; + return [nameLink, passed, failed, skipped, time]; + }); + if (tableData.length > 0) { const resultsTable = table(['Report', 'Passed', 'Failed', 'Skipped', 'Time'], [Align.Left, Align.Right, Align.Right, Align.Right, Align.Right], ...tableData); sections.push(resultsTable); } if (options.onlySummary === false) { - const suitesReports = testRuns.map((tr, i) => getSuitesReport(tr, i, options)).flat(); + const suitesReports = filteredTestRuns.map((tr, i) => getSuitesReport(tr, i, options)).flat(); sections.push(...suitesReports); } if (shouldCollapse) { @@ -58949,6 +58956,7 @@ class TestReporter { reporter = getInput('reporter', { required: true }); listSuites = getInput('list-suites', { required: true }); listTests = getInput('list-tests', { required: true }); + listFiles = getInput('list-files', { required: true }); maxAnnotations = parseInt(getInput('max-annotations', { required: true })); failOnError = getInput('fail-on-error', { required: true }) === 'true'; failOnEmpty = getInput('fail-on-empty', { required: true }) === 'true'; @@ -58972,6 +58980,10 @@ class TestReporter { setFailed(`Input parameter 'list-tests' has invalid value`); return; } + if (this.listFiles !== 'all' && this.listFiles !== 'failed' && this.listFiles !== 'none') { + setFailed(`Input parameter 'list-files' has invalid value`); + return; + } if (this.collapsed !== 'auto' && this.collapsed !== 'always' && this.collapsed !== 'never') { setFailed(`Input parameter 'collapsed' has invalid value`); return; @@ -59056,7 +59068,7 @@ class TestReporter { throw error; } } - const { listSuites, listTests, slugPrefix, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed } = this; + const { listSuites, listTests, slugPrefix, listFiles, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed } = this; const passed = results.reduce((sum, tr) => sum + tr.passed, 0); const failed = results.reduce((sum, tr) => sum + tr.failed, 0); const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0); @@ -59067,6 +59079,7 @@ class TestReporter { listSuites, listTests, slugPrefix, + listFiles, baseUrl, onlySummary, useActionsSummary, @@ -59096,6 +59109,7 @@ class TestReporter { listSuites, listTests, slugPrefix, + listFiles, baseUrl, onlySummary, useActionsSummary, diff --git a/src/main.ts b/src/main.ts index fc6ea16..eb1b3a5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,6 +44,7 @@ class TestReporter { readonly reporter = core.getInput('reporter', {required: true}) 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 listFiles = core.getInput('list-files', {required: true}) as 'all' | 'failed' | 'none' readonly maxAnnotations = parseInt(core.getInput('max-annotations', {required: true})) readonly failOnError = core.getInput('fail-on-error', {required: true}) === 'true' readonly failOnEmpty = core.getInput('fail-on-empty', {required: true}) === 'true' @@ -71,6 +72,11 @@ class TestReporter { return } + if (this.listFiles !== 'all' && this.listFiles !== 'failed' && this.listFiles !== 'none') { + core.setFailed(`Input parameter 'list-files' has invalid value`) + return + } + if (this.collapsed !== 'auto' && this.collapsed !== 'always' && this.collapsed !== 'never') { core.setFailed(`Input parameter 'collapsed' has invalid value`) return @@ -177,7 +183,17 @@ class TestReporter { } } - const {listSuites, listTests, slugPrefix, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed} = this + const { + listSuites, + listTests, + slugPrefix, + listFiles, + onlySummary, + useActionsSummary, + badgeTitle, + reportTitle, + collapsed + } = this const passed = results.reduce((sum, tr) => sum + tr.passed, 0) const failed = results.reduce((sum, tr) => sum + tr.failed, 0) @@ -192,6 +208,7 @@ class TestReporter { listSuites, listTests, slugPrefix, + listFiles, baseUrl, onlySummary, useActionsSummary, @@ -224,6 +241,7 @@ class TestReporter { listSuites, listTests, slugPrefix, + listFiles, baseUrl, onlySummary, useActionsSummary, diff --git a/src/report/get-report.ts b/src/report/get-report.ts index 325e28c..cac5596 100644 --- a/src/report/get-report.ts +++ b/src/report/get-report.ts @@ -12,6 +12,7 @@ export interface ReportOptions { listSuites: 'all' | 'failed' | 'none' listTests: 'all' | 'failed' | 'none' slugPrefix: string + listFiles: 'all' | 'failed' | 'none' baseUrl: string onlySummary: boolean useActionsSummary: boolean @@ -24,6 +25,7 @@ export const DEFAULT_OPTIONS: ReportOptions = { listSuites: 'all', listTests: 'all', slugPrefix: '', + listFiles: 'all', baseUrl: '', onlySummary: false, useActionsSummary: true, @@ -173,21 +175,29 @@ function getTestRunsReport(testRuns: TestRunResult[], options: ReportOptions): s sections.push(` `) } - if (testRuns.length > 0 || options.onlySummary) { - const tableData = testRuns - .map((tr, originalIndex) => ({tr, originalIndex})) - .filter(({tr}) => tr.passed > 0 || tr.failed > 0 || tr.skipped > 0) - .map(({tr, originalIndex}) => { - const time = formatTime(tr.time) - const name = tr.path - const addr = options.baseUrl + makeRunSlug(originalIndex, options).link - const nameLink = link(name, addr) - const passed = tr.passed > 0 ? `${tr.passed} ${Icon.success}` : '' - const failed = tr.failed > 0 ? `${tr.failed} ${Icon.fail}` : '' - const skipped = tr.skipped > 0 ? `${tr.skipped} ${Icon.skip}` : '' - return [nameLink, passed, failed, skipped, time] - }) + // Filter test runs based on list-files option + const filteredTestRuns = + options.listFiles === 'failed' + ? testRuns.filter(tr => tr.result === 'failed') + : options.listFiles === 'none' + ? [] + : testRuns + const tableData = filteredTestRuns + .map((tr, originalIndex) => ({tr, originalIndex})) + .filter(({tr}) => tr.passed > 0 || tr.failed > 0 || tr.skipped > 0) + .map(({tr, originalIndex}) => { + const time = formatTime(tr.time) + const name = tr.path + const addr = options.baseUrl + makeRunSlug(originalIndex, options).link + const nameLink = link(name, addr) + const passed = tr.passed > 0 ? `${tr.passed} ${Icon.success}` : '' + const failed = tr.failed > 0 ? `${tr.failed} ${Icon.fail}` : '' + const skipped = tr.skipped > 0 ? `${tr.skipped} ${Icon.skip}` : '' + return [nameLink, passed, failed, skipped, time] + }) + + if (tableData.length > 0) { const resultsTable = table( ['Report', 'Passed', 'Failed', 'Skipped', 'Time'], [Align.Left, Align.Right, Align.Right, Align.Right, Align.Right], @@ -197,7 +207,7 @@ function getTestRunsReport(testRuns: TestRunResult[], options: ReportOptions): s } if (options.onlySummary === false) { - const suitesReports = testRuns.map((tr, i) => getSuitesReport(tr, i, options)).flat() + const suitesReports = filteredTestRuns.map((tr, i) => getSuitesReport(tr, i, options)).flat() sections.push(...suitesReports) }