From 2380ffd7fe752cf84fe1c36a41851103f145d792 Mon Sep 17 00:00:00 2001 From: alexanderthoren Date: Mon, 23 Feb 2026 16:12:01 +0100 Subject: [PATCH 1/2] feat: add sort-suites input to order suites by time descending Add a new 'sort-suites' input parameter that accepts 'name' (default, alphabetical) or 'time-desc' (slowest first). This allows consumers to surface the slowest test suites at the top of the report. Co-authored-by: Cursor --- action.yml | 7 +++++++ dist/index.js | 21 +++++++++++++++++---- src/main.ts | 10 +++++++++- src/report/get-report.ts | 12 +++++++++--- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/action.yml b/action.yml index 8dbc85c..e98fc93 100644 --- a/action.yml +++ b/action.yml @@ -46,6 +46,13 @@ inputs: - none required: false default: 'all' + sort-suites: + description: | + Sort order for test suites. Supported options: + - name: Sort alphabetically by name (default) + - time-desc: Sort by execution time, slowest first + required: false + default: 'name' list-tests: description: | Limits which test cases are listed. Supported options: diff --git a/dist/index.js b/dist/index.js index aae80e7..588c7b4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -303,6 +303,7 @@ class TestReporter { pathReplaceBackslashes = core.getInput('path-replace-backslashes', { required: false }) === 'true'; reporter = core.getInput('reporter', { required: true }); listSuites = core.getInput('list-suites', { required: true }); + sortSuites = core.getInput('sort-suites', { required: false }); listTests = core.getInput('list-tests', { required: true }); maxAnnotations = parseInt(core.getInput('max-annotations', { required: true })); failOnError = core.getInput('fail-on-error', { required: true }) === 'true'; @@ -322,6 +323,10 @@ class TestReporter { core.setFailed(`Input parameter 'list-suites' has invalid value`); return; } + if (this.sortSuites !== 'name' && this.sortSuites !== 'time-desc') { + core.setFailed(`Input parameter 'sort-suites' has invalid value`); + return; + } if (this.listTests !== 'all' && this.listTests !== 'failed' && this.listTests !== 'none') { core.setFailed(`Input parameter 'list-tests' has invalid value`); return; @@ -409,7 +414,7 @@ class TestReporter { throw error; } } - const { listSuites, listTests, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed } = this; + const { listSuites, sortSuites, listTests, 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); @@ -418,6 +423,7 @@ class TestReporter { if (this.useActionsSummary) { const summary = (0, get_report_1.getReport)(results, { listSuites, + sortSuites, listTests, baseUrl, onlySummary, @@ -446,6 +452,7 @@ class TestReporter { baseUrl = createResp.data.html_url; const summary = (0, get_report_1.getReport)(results, { listSuites, + sortSuites, listTests, baseUrl, onlySummary, @@ -2446,6 +2453,7 @@ const MAX_ACTIONS_SUMMARY_LENGTH = 1048576; exports.DEFAULT_OPTIONS = { listSuites: 'all', listTests: 'all', + sortSuites: 'name', baseUrl: '', onlySummary: false, useActionsSummary: true, @@ -2454,7 +2462,7 @@ exports.DEFAULT_OPTIONS = { collapsed: 'auto' }; function getReport(results, options = exports.DEFAULT_OPTIONS, shortSummary = '') { - applySort(results); + applySort(results, options); const opts = { ...options }; let lines = renderReport(results, opts, shortSummary); let report = lines.join('\n'); @@ -2502,10 +2510,15 @@ function trimReport(lines, options) { reportLines.push(errorMsg); return reportLines.join('\n'); } -function applySort(results) { +function applySort(results, options) { results.sort((a, b) => a.path.localeCompare(b.path, node_utils_1.DEFAULT_LOCALE)); for (const res of results) { - res.suites.sort((a, b) => a.name.localeCompare(b.name, node_utils_1.DEFAULT_LOCALE)); + if (options.sortSuites === 'time-desc') { + res.suites.sort((a, b) => b.time - a.time); + } + else { + res.suites.sort((a, b) => a.name.localeCompare(b.name, node_utils_1.DEFAULT_LOCALE)); + } } } function getByteLength(text) { diff --git a/src/main.ts b/src/main.ts index 9b24d38..9c15e9b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,6 +42,7 @@ class TestReporter { readonly pathReplaceBackslashes = core.getInput('path-replace-backslashes', {required: false}) === 'true' readonly reporter = core.getInput('reporter', {required: true}) readonly listSuites = core.getInput('list-suites', {required: true}) as 'all' | 'failed' | 'none' + readonly sortSuites = core.getInput('sort-suites', {required: false}) as 'name' | 'time-desc' readonly listTests = core.getInput('list-tests', {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' @@ -64,6 +65,11 @@ class TestReporter { return } + if (this.sortSuites !== 'name' && this.sortSuites !== 'time-desc') { + core.setFailed(`Input parameter 'sort-suites' has invalid value`) + return + } + if (this.listTests !== 'all' && this.listTests !== 'failed' && this.listTests !== 'none') { core.setFailed(`Input parameter 'list-tests' has invalid value`) return @@ -174,7 +180,7 @@ class TestReporter { } } - const {listSuites, listTests, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed} = this + const {listSuites, sortSuites, listTests, 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) @@ -187,6 +193,7 @@ class TestReporter { results, { listSuites, + sortSuites, listTests, baseUrl, onlySummary, @@ -218,6 +225,7 @@ class TestReporter { baseUrl = createResp.data.html_url as string const summary = getReport(results, { listSuites, + sortSuites, listTests, baseUrl, onlySummary, diff --git a/src/report/get-report.ts b/src/report/get-report.ts index 02b9d49..015589c 100644 --- a/src/report/get-report.ts +++ b/src/report/get-report.ts @@ -11,6 +11,7 @@ const MAX_ACTIONS_SUMMARY_LENGTH = 1048576 export interface ReportOptions { listSuites: 'all' | 'failed' | 'none' listTests: 'all' | 'failed' | 'none' + sortSuites: 'name' | 'time-desc' baseUrl: string onlySummary: boolean useActionsSummary: boolean @@ -22,6 +23,7 @@ export interface ReportOptions { export const DEFAULT_OPTIONS: ReportOptions = { listSuites: 'all', listTests: 'all', + sortSuites: 'name', baseUrl: '', onlySummary: false, useActionsSummary: true, @@ -35,7 +37,7 @@ export function getReport( options: ReportOptions = DEFAULT_OPTIONS, shortSummary = '' ): string { - applySort(results) + applySort(results, options) const opts = {...options} let lines = renderReport(results, opts, shortSummary) @@ -94,10 +96,14 @@ function trimReport(lines: string[], options: ReportOptions): string { return reportLines.join('\n') } -function applySort(results: TestRunResult[]): void { +function applySort(results: TestRunResult[], options: ReportOptions): void { results.sort((a, b) => a.path.localeCompare(b.path, DEFAULT_LOCALE)) for (const res of results) { - res.suites.sort((a, b) => a.name.localeCompare(b.name, DEFAULT_LOCALE)) + if (options.sortSuites === 'time-desc') { + res.suites.sort((a, b) => b.time - a.time) + } else { + res.suites.sort((a, b) => a.name.localeCompare(b.name, DEFAULT_LOCALE)) + } } } From edb6902bbeb8b2f5e0655d558b4c1d57a675a533 Mon Sep 17 00:00:00 2001 From: alexanderthoren Date: Mon, 23 Feb 2026 16:37:18 +0100 Subject: [PATCH 2/2] test-reporter release v2.6.0 Co-authored-by: Cursor --- CHANGELOG.md | 3 +++ package-lock.json | 14 ++------------ package.json | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9163f49..79915a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 2.6.0 +* Feature: Add `sort-suites` input to order test suites by execution time (descending) + ## 2.5.0 * Feature: Add Nette Tester support with `tester-junit` reporter https://github.com/dorny/test-reporter/pull/707 * Maintenance: Bump actions/upload-artifact from 5 to 6 https://github.com/dorny/test-reporter/pull/695 diff --git a/package-lock.json b/package-lock.json index bc6dbfb..6d3a7aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "test-reporter", - "version": "2.5.0", + "version": "2.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "test-reporter", - "version": "2.5.0", + "version": "2.6.0", "license": "MIT", "dependencies": { "@actions/core": "^1.11.1", @@ -146,7 +146,6 @@ "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1516,7 +1515,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -2444,7 +2442,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2914,7 +2911,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -3759,7 +3755,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4006,7 +4001,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5641,7 +5635,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -7117,7 +7110,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7229,7 +7221,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -8366,7 +8357,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 5554c9a..d9b6eac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "test-reporter", - "version": "2.5.0", + "version": "2.6.0", "private": true, "description": "Presents test results from popular testing frameworks as Github check run", "main": "lib/main.js",