@@ -4,6 +4,8 @@ import type { Vitest } from '../core'
44import type { TestProject } from '../project'
55import type { Reporter } from '../types/reporter'
66import type { TestCase , TestModule } from './reported-tasks'
7+ import { writeFileSync } from 'node:fs'
8+ import { relative } from 'node:path'
79import { stripVTControlCharacters } from 'node:util'
810import { getFullName , getTasks } from '@vitest/runner/utils'
911import { capturePrintError } from '../printError'
@@ -109,6 +111,19 @@ export class GithubActionsReporter implements Reporter {
109111 } )
110112 this . ctx . logger . log ( `\n${ formatted } ` )
111113 }
114+
115+ if ( process . env . GITHUB_STEP_SUMMARY ) {
116+ try {
117+ writeFileSync (
118+ process . env . GITHUB_STEP_SUMMARY ,
119+ renderSummary ( collectSummaryData ( testModules ) ) ,
120+ { flag : 'a' } ,
121+ )
122+ }
123+ catch ( error ) {
124+ this . ctx . logger . warn ( 'Could not write summary to $GITHUB_STEP_SUMMARY' , error )
125+ }
126+ }
112127 }
113128}
114129
@@ -165,3 +180,111 @@ function escapeProperty(s: string): string {
165180 . replace ( / : / g, '%3A' )
166181 . replace ( / , / g, '%2C' )
167182}
183+
184+ interface SummaryData {
185+ flakyTests : Array < {
186+ path : {
187+ relative : string
188+ absolute : string
189+ }
190+ tests : Array < {
191+ testName : string
192+ line : number | undefined
193+ retries : {
194+ allowed : number
195+ count : number
196+ ratio : number
197+ }
198+ } >
199+ } >
200+ }
201+
202+ function collectSummaryData ( testModules : ReadonlyArray < TestModule > ) : SummaryData {
203+ const summaryData : SummaryData = { flakyTests : [ ] }
204+
205+ for ( const module of testModules ) {
206+ const flakyTests : SummaryData [ 'flakyTests' ] [ number ] = {
207+ path : { relative : module . relativeModuleId , absolute : module . moduleId } ,
208+ tests : [ ] ,
209+ }
210+
211+ for ( const test of module . children . allTests ( ) ) {
212+ const diagnostic = test . diagnostic ( )
213+
214+ if ( diagnostic ?. flaky ) {
215+ const retriesAllowed = typeof test . options . retry === 'number'
216+ ? test . options . retry
217+ : ( test . options . retry ?. count
218+ // falling back to `retryCount` as this is used as the denominator to compute `retryRatio`
219+ ?? diagnostic . retryCount )
220+ const retriesRatio = diagnostic . retryCount / retriesAllowed
221+
222+ flakyTests . tests . push ( {
223+ retries : {
224+ allowed : retriesAllowed ,
225+ count : diagnostic . retryCount ,
226+ ratio : retriesRatio ,
227+ } ,
228+ line : test . task . location ?. line ,
229+ testName : test . task . fullTestName ,
230+ } )
231+ }
232+ }
233+
234+ if ( flakyTests . tests . length > 0 ) {
235+ flakyTests . tests . sort ( ( a , b ) => b . retries . ratio - a . retries . ratio )
236+
237+ summaryData . flakyTests . push ( flakyTests )
238+ }
239+ }
240+
241+ return summaryData
242+ }
243+
244+ function createGitHubFileLinkCreator ( ) : ( path : string , line ?: number ) => string | null {
245+ const repository = process . env . GITHUB_REPOSITORY
246+ const commitHash = process . env . GITHUB_SHA
247+ const rootPath = process . env . GITHUB_WORKSPACE
248+
249+ if ( repository !== undefined && commitHash !== undefined && rootPath !== undefined ) {
250+ return ( path , line ) => {
251+ const lineFragment = line !== undefined ? `#L${ line } ` : ''
252+
253+ return `https://github.com/${ repository } /blob/${ commitHash } /${ relative ( rootPath , path ) } ${ lineFragment } `
254+ }
255+ }
256+
257+ return ( ) => null
258+ }
259+
260+ function mdLink ( text : string , url : string | null ) : string {
261+ return url === null ? text : `[${ text } ](${ url } )`
262+ }
263+
264+ function renderSummary ( summaryData : SummaryData ) : string {
265+ const fileLinkCreator = createGitHubFileLinkCreator ( )
266+
267+ let summary = '## Vitest Test Report\n'
268+
269+ if ( summaryData . flakyTests . length > 0 ) {
270+ summary += '\n### Flaky Tests\n\nThese tests passed only after one or more retries, indicating potential instability.\n'
271+
272+ for ( const flakyTests of summaryData . flakyTests ) {
273+ summary += `\n##### \`${ flakyTests . path . relative } \` (${ flakyTests . tests . length } flaky tests)\n`
274+
275+ for ( const flakyTest of flakyTests . tests ) {
276+ const retriesText = `passed on retry ${ flakyTest . retries . count } out of ${ flakyTest . retries . allowed } `
277+
278+ summary += `\n- ${ mdLink ( `\`${ flakyTest . testName } \`` , fileLinkCreator ( flakyTests . path . absolute , flakyTest . line ) ) } (${ flakyTest . retries . ratio >= 0.8 ? `**${ retriesText } **` : retriesText } )`
279+ }
280+
281+ summary += '\n'
282+ }
283+ }
284+
285+ if ( ! summary . endsWith ( '\n' ) ) {
286+ summary += '\n'
287+ }
288+
289+ return summary
290+ }
0 commit comments