@@ -40,16 +40,15 @@ function renderReplayTestSummary(
4040 for ( const entry of data . failures ) {
4141 renderFailedTestResult ( entry ) ;
4242 }
43- for ( const entry of flaky ) {
44- renderFlakyTestResult ( entry ) ;
45- }
4643 }
4744
4845 const durationMs = typeof data . durationMs === 'number' ? data . durationMs : undefined ;
4946 const flakySuffix = flaky . length > 0 ? `, ${ flaky . length } flaky` : '' ;
47+ const durationSuffix = durationMs !== undefined ? ` in ${ formatDurationSeconds ( durationMs ) } ` : '' ;
5048 process . stdout . write (
51- `Test summary: ${ data . passed } passed, ${ data . failed } failed${ flakySuffix } ${ durationMs !== undefined ? ` in ${ durationMs } ms` : '' } \n` ,
49+ `Test summary: ${ data . passed } passed, ${ data . failed } failed${ flakySuffix } ${ durationSuffix } \n` ,
5250 ) ;
51+ renderFlakyTestSummary ( flaky ) ;
5352 return getReplayTestExitCode ( data ) ;
5453}
5554
@@ -59,11 +58,10 @@ function renderVerboseTestResult(result: ReplaySuiteTestResult): void {
5958 return ;
6059 }
6160
62- const prefix = replayResultPrefix ( result ) ;
63- const attemptSuffix =
64- 'attempts' in result && result . attempts > 1 ? ` after ${ result . attempts } attempts` : '' ;
65- const durationSuffix = result . durationMs > 0 ? ` (${ result . durationMs } ms)` : '' ;
66- process . stdout . write ( `${ prefix } ${ result . file } ${ attemptSuffix } ${ durationSuffix } \n` ) ;
61+ const durationSuffix = formatReplayTestDurationSuffix ( result ) ;
62+ process . stdout . write (
63+ `${ replayResultPrefix ( result ) } ${ replayTestDisplayName ( result ) } ${ durationSuffix } \n` ,
64+ ) ;
6765 if ( result . status === 'skipped' ) {
6866 process . stdout . write ( ` ${ result . message ?? 'skipped' } \n` ) ;
6967 }
@@ -73,16 +71,18 @@ function renderFailedTestResult(
7371 result : Extract < ReplaySuiteTestResult , { status : 'failed' } > ,
7472) : void {
7573 const attemptSuffix = result . attempts > 1 ? ` after ${ result . attempts } attempts` : '' ;
76- const durationSuffix = result . durationMs > 0 ? ` (${ result . durationMs } ms)` : '' ;
77- process . stdout . write ( `FAIL ${ result . file } ${ attemptSuffix } ${ durationSuffix } \n` ) ;
74+ const durationSuffix = formatReplayTestDurationSuffix ( result ) ;
75+ process . stdout . write (
76+ `FAIL ${ replayFailedTestDisplayName ( result ) } ${ attemptSuffix } ${ durationSuffix } \n` ,
77+ ) ;
7878 process . stdout . write ( ` ${ result . error ?. message ?? 'Unknown test failure' } \n` ) ;
7979 for ( const line of replayFailureConsoleLines ( result ) ) {
8080 process . stdout . write ( ` ${ line } \n` ) ;
8181 }
8282}
8383
8484function replayResultPrefix ( result : ReplaySuiteTestResult ) : string {
85- if ( result . status === 'passed' ) return result . attempts > 1 ? 'FLAKY' : 'PASS' ;
85+ if ( result . status === 'passed' ) return 'PASS' ;
8686 if ( result . status === 'skipped' ) return 'SKIP' ;
8787 return 'INFO' ;
8888}
@@ -98,17 +98,83 @@ function replayFailureConsoleLines(
9898 ] . filter ( Boolean ) ;
9999}
100100
101- function renderFlakyTestResult ( result : Extract < ReplaySuiteTestResult , { status : 'passed' } > ) : void {
102- const durationSuffix = result . durationMs > 0 ? ` (${ result . durationMs } ms)` : '' ;
103- process . stdout . write ( `FLAKY ${ result . file } after ${ result . attempts } attempts${ durationSuffix } \n` ) ;
104- }
105-
106101function isFlakyReplayTestResult (
107102 result : ReplaySuiteTestResult ,
108103) : result is Extract < ReplaySuiteTestResult , { status : 'passed' } > {
109104 return result . status === 'passed' && result . attempts > 1 ;
110105}
111106
107+ function renderFlakyTestSummary (
108+ results : Array < Extract < ReplaySuiteTestResult , { status : 'passed' } > > ,
109+ ) : void {
110+ if ( results . length === 0 ) return ;
111+ process . stdout . write ( 'Flaky tests:\n' ) ;
112+ for ( const result of results ) {
113+ process . stdout . write (
114+ ` PASS ${ replayTestDisplayName ( result ) } after ${ result . attempts } attempts${ formatFlakyReplayDurationSuffix ( result ) } \n` ,
115+ ) ;
116+ for ( const failure of result . attemptFailures ?? [ ] ) {
117+ const attemptDuration =
118+ typeof failure . durationMs === 'number'
119+ ? ` (${ formatDurationSeconds ( failure . durationMs ) } )`
120+ : '' ;
121+ process . stdout . write (
122+ ` attempt ${ failure . attempt } failed${ attemptDuration } : ${ failure . message } \n` ,
123+ ) ;
124+ }
125+ }
126+ }
127+
128+ function replayTestDisplayName ( result : ReplaySuiteTestResult ) : string {
129+ const title = replayTestTitle ( result ) ;
130+ if ( title && title . length > 0 ) return JSON . stringify ( title ) ;
131+ return path . basename ( result . file ) ;
132+ }
133+
134+ function replayFailedTestDisplayName (
135+ result : Extract < ReplaySuiteTestResult , { status : 'failed' } > ,
136+ ) : string {
137+ const title = replayTestTitle ( result ) ;
138+ const filename = path . basename ( result . file ) ;
139+ return title && title . length > 0 ? `${ JSON . stringify ( title ) } in ${ filename } ` : filename ;
140+ }
141+
142+ function replayTestCaseName ( result : ReplaySuiteTestResult ) : string {
143+ return replayTestTitle ( result ) ?? path . basename ( result . file ) ;
144+ }
145+
146+ function replayTestTitle ( result : ReplaySuiteTestResult ) : string | undefined {
147+ const title = result . title ?. trim ( ) ;
148+ return title && title . length > 0 ? title : undefined ;
149+ }
150+
151+ function formatReplayTestDurationSuffix ( result : ReplaySuiteTestResult ) : string {
152+ if ( result . status === 'passed' && result . attempts > 1 ) {
153+ return formatFlakyReplayDurationSuffix ( result ) ;
154+ }
155+ if ( result . status === 'failed' && result . attempts > 1 && result . durationMs > 0 ) {
156+ return ` (total ${ formatDurationSeconds ( result . durationMs ) } )` ;
157+ }
158+
159+ const durationMs =
160+ result . status === 'passed' && typeof result . finalAttemptDurationMs === 'number'
161+ ? result . finalAttemptDurationMs
162+ : result . durationMs ;
163+ return durationMs > 0 ? ` (${ formatDurationSeconds ( durationMs ) } )` : '' ;
164+ }
165+
166+ function formatFlakyReplayDurationSuffix (
167+ result : Extract < ReplaySuiteTestResult , { status : 'passed' } > ,
168+ ) : string {
169+ const timings = [
170+ typeof result . finalAttemptDurationMs === 'number'
171+ ? `passed attempt ${ formatDurationSeconds ( result . finalAttemptDurationMs ) } `
172+ : '' ,
173+ result . durationMs > 0 ? `total ${ formatDurationSeconds ( result . durationMs ) } ` : '' ,
174+ ] . filter ( Boolean ) ;
175+ return timings . length > 0 ? ` (${ timings . join ( ', ' ) } )` : '' ;
176+ }
177+
112178function getReplayTestExitCode ( data : ReplaySuiteResult ) : number {
113179 return data . failed > 0 ? 1 : 0 ;
114180}
@@ -144,7 +210,7 @@ function buildReplayJunitXml(suite: ReplaySuiteResult): string {
144210}
145211
146212function renderJUnitTestCase ( test : ReplaySuiteTestResult ) : string [ ] {
147- const name = xmlEscape ( path . basename ( test . file ) ) ;
213+ const name = xmlEscape ( replayTestCaseName ( test ) ) ;
148214 const className = xmlEscape (
149215 path . dirname ( test . file ) === '.' ? test . file : path . dirname ( test . file ) ,
150216 ) ;
@@ -239,6 +305,13 @@ function formatJUnitSeconds(durationMs: number): string {
239305 return ( Math . max ( 0 , durationMs ) / 1000 ) . toFixed ( 3 ) ;
240306}
241307
308+ function formatDurationSeconds ( durationMs : number ) : string {
309+ const seconds = Math . max ( 0 , durationMs ) / 1000 ;
310+ if ( seconds >= 10 ) return `${ seconds . toFixed ( 1 ) } s` ;
311+ if ( seconds >= 1 ) return `${ seconds . toFixed ( 2 ) } s` ;
312+ return `${ seconds . toFixed ( 3 ) . replace ( / 0 + $ / , '' ) . replace ( / \. $ / , '' ) } s` ;
313+ }
314+
242315function xmlEscape ( value : string ) : string {
243316 return value
244317 . replaceAll ( '&' , '&' )
0 commit comments