@@ -15,6 +15,7 @@ import { getCurrentTheme } from './helpers/themeUtils';
1515
1616const LAUNCH_RETRY_ATTEMPTS = 5 ;
1717const LAUNCH_RETRY_TIMEOUT = 10000 ;
18+ const FAILED_TESTS_RETRY_ATTEMPTS = 3 ;
1819
1920const wait = async (
2021 timeout : number ,
@@ -55,6 +56,7 @@ interface ParsedArgs {
5556 shadowDom : boolean ;
5657 skipUnstable : boolean ;
5758 disableScreenshots : boolean ;
59+ retryFailed : boolean ;
5860}
5961
6062const TESTCAFE_CONFIG : Partial < TestCafeConfigurationOptions > = {
@@ -101,7 +103,7 @@ function getArgs(): ParsedArgs {
101103 concurrency : 0 ,
102104 browsers : 'chrome' ,
103105 test : '' ,
104- reporter : process . env . CI === 'true' ? 'list' : ' spec',
106+ reporter : ' spec-time ',
105107 componentFolder : '' ,
106108 file : '*' ,
107109 cache : true ,
@@ -112,6 +114,7 @@ function getArgs(): ParsedArgs {
112114 shadowDom : false ,
113115 skipUnstable : true ,
114116 disableScreenshots : false ,
117+ retryFailed : true ,
115118 } ,
116119 } ) as ParsedArgs ;
117120}
@@ -157,77 +160,89 @@ async function main() {
157160 // eslint-disable-next-line no-console
158161 console . info ( 'Browsers:' , browsers ) ;
159162
160- const runner : Runner = testCafe . createRunner ( )
161- . browsers ( browsers )
162- . reporter ( reporter )
163- . src ( [ `./tests/${ componentFolder } /${ file } .ts` ] ) ;
163+ const failedTests : Set < string > = new Set ( ) ;
164164
165- runner . compilerOptions ( {
166- typescript : {
167- customCompilerModulePath : '../../node_modules/typescript' ,
168- } ,
169- } ) ;
170-
171- runner . concurrency ( args . concurrency || 4 ) ;
165+ const createRunner = ( filterByFailedTests = false ) => {
166+ const runner : Runner = testCafe ! . createRunner ( )
167+ . browsers ( browsers )
168+ . reporter ( reporter )
169+ . src ( [ `./tests/${ componentFolder } /${ file } .ts` ] ) ;
172170
173- const filters : FilterFunction [ ] = [ ] ;
171+ runner . compilerOptions ( {
172+ typescript : {
173+ customCompilerModulePath : '../../node_modules/typescript' ,
174+ } ,
175+ } ) ;
174176
175- if ( indices ) {
176- const [ current , total ] = indices . split ( / _ | o f | \\ | \/ / ig) . map ( ( x ) => + x ) ;
177- const fixtures = globSync ( [ `./tests/${ componentFolder } /*.ts` ] ) ;
178- const fixtureChunks = split ( fixtures , total ) ;
179- const targetFixtureChunk = fixtureChunks [ current - 1 ] ?? [ ] ;
180- const targetFixtureChunkSet = new Set ( targetFixtureChunk ) ;
177+ runner . concurrency ( filterByFailedTests ? 1 : ( args . concurrency || 4 ) ) ;
178+
179+ const filters : FilterFunction [ ] = [ ] ;
180+
181+ if ( indices ) {
182+ const [ current , total ] = indices . split ( / _ | o f | \\ | \/ / ig) . map ( ( x ) => + x ) ;
183+ const fixtures = globSync ( [ `./tests/${ componentFolder } /*.ts` ] ) ;
184+ const fixtureChunks = split ( fixtures , total ) ;
185+ const targetFixtureChunk = fixtureChunks [ current - 1 ] ?? [ ] ;
186+ const targetFixtureChunkSet = new Set ( targetFixtureChunk ) ;
187+
188+ if ( ! filterByFailedTests ) {
189+ /* eslint-disable no-console */
190+ console . info ( ' === test run config ===' ) ;
191+ console . info ( ` > indices: current = ${ current } | total = ${ total } ` ) ;
192+ console . info ( ' > glob: ' , [ `./tests/${ componentFolder } /*.ts` ] ) ;
193+ console . info ( ' > all fixtures: ' , fixtureChunks ) ;
194+ console . info ( ' > fixtures: ' , targetFixtureChunk , '\n' ) ;
195+ /* eslint-enable no-console */
196+ }
181197
182- /* eslint-disable no-console */
183- console . info ( ' === test run config ===' ) ;
184- console . info ( ` > indices: current = ${ current } | total = ${ total } ` ) ;
185- console . info ( ' > glob: ' , [ `./tests/${ componentFolder } /*.ts` ] ) ;
186- console . info ( ' > all fixtures: ' , fixtureChunks ) ;
187- console . info ( ' > fixtures: ' , targetFixtureChunk , '\n' ) ;
188- /* eslint-enable no-console */
198+ filters . push ( (
199+ _testName : string ,
200+ _fixtureName : string ,
201+ fixturePath : string ,
202+ ) => {
203+ const testPath = fixturePath . split ( '/testcafe-devextreme/' ) [ 1 ] ;
204+ return targetFixtureChunkSet . has ( testPath ) ;
205+ } ) ;
206+ }
189207
190- filters . push ( (
191- _testName : string ,
192- _fixtureName : string ,
193- fixturePath : string ,
194- ) => {
195- const testPath = fixturePath . split ( '/testcafe-devextreme/' ) [ 1 ] ;
196- return targetFixtureChunkSet . has ( testPath ) ;
197- } ) ;
198- }
208+ if ( testName ) {
209+ filters . push ( ( name : string ) => name === testName ) ;
210+ }
199211
200- if ( testName ) {
201- filters . push ( ( name : string ) => name === testName ) ;
202- }
212+ if ( filterByFailedTests && failedTests . size > 0 ) {
213+ filters . push ( ( name : string ) => failedTests . has ( name ) ) ;
214+ }
203215
204- if ( args . skipUnstable ) {
205- filters . push ( (
206- _testName : string ,
207- _fixtureName : string ,
208- _fixturePath : string ,
209- testMeta ?: any ,
210- ) => ! ( testMeta ) ?. unstable ) ;
211- }
216+ if ( args . skipUnstable ) {
217+ filters . push ( (
218+ _testName : string ,
219+ _fixtureName : string ,
220+ _fixturePath : string ,
221+ testMeta ?: any ,
222+ ) => ! ( testMeta ) ?. unstable ) ;
223+ }
212224
213- if ( filters . length ) {
214- runner . filter ( ( ...filterArgs : Parameters < FilterFunction > ) => {
215- // eslint-disable-next-line @typescript-eslint/prefer-for-of
216- for ( let i = 0 ; i < filters . length ; i += 1 ) {
217- if ( ! filters [ i ] ( ...filterArgs ) ) {
218- return false ;
225+ if ( filters . length ) {
226+ runner . filter ( ( ...filterArgs : Parameters < FilterFunction > ) => {
227+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
228+ for ( let i = 0 ; i < filters . length ; i += 1 ) {
229+ if ( ! filters [ i ] ( ...filterArgs ) ) {
230+ return false ;
231+ }
219232 }
220- }
221- return true ;
222- } ) ;
223- }
233+ return true ;
234+ } ) ;
235+ }
224236
225- if ( args . cache ) {
226- ( runner as any ) . cache = args . cache ;
227- }
237+ if ( args . cache ) {
238+ ( runner as any ) . cache = args . cache ;
239+ }
240+
241+ return runner ;
242+ } ;
228243
229244 const runOptions : RunOptions = {
230- quarantineMode : { successThreshold : 1 , attemptLimit : 2 } ,
245+ quarantineMode : false ,
231246 // @ts -expect-error ts-error
232247 hooks : {
233248 test : {
@@ -266,6 +281,14 @@ async function main() {
266281 } ,
267282 after : async ( t : TestController ) => {
268283 await clearTestPage ( t ) ;
284+
285+ if ( args . retryFailed ) {
286+ // @ts -expect-error ts-errors
287+ const { test, errs } = t . testRun ;
288+ if ( errs && errs . length > 0 ) {
289+ failedTests . add ( test . name ) ;
290+ }
291+ }
269292 } ,
270293 } ,
271294 } ,
@@ -275,7 +298,75 @@ async function main() {
275298 runOptions . disableScreenshots = true ;
276299 }
277300
278- const failedCount = await retry ( ( ) => runner . run ( runOptions ) , LAUNCH_RETRY_ATTEMPTS ) ;
301+ // First run - all tests
302+ const runner = createRunner ( false ) ;
303+ let failedCount = await retry ( ( ) => runner . run ( runOptions ) , LAUNCH_RETRY_ATTEMPTS ) ;
304+
305+ // Retry failed tests multiple times if enabled and there are failures
306+ if ( args . retryFailed && failedTests . size > 0 && failedCount > 0 ) {
307+ const initialFailedCount = failedTests . size ;
308+ let attemptsLeft = FAILED_TESTS_RETRY_ATTEMPTS ;
309+
310+ while ( attemptsLeft > 0 && failedTests . size > 0 && failedCount > 0 ) {
311+ const attemptNumber = FAILED_TESTS_RETRY_ATTEMPTS - attemptsLeft + 1 ;
312+
313+ /* eslint-disable no-console */
314+ console . info ( '\n' ) ;
315+ console . info ( '=' . repeat ( 60 ) ) ;
316+ console . info ( `RETRY ATTEMPT ${ attemptNumber } /${ FAILED_TESTS_RETRY_ATTEMPTS } ` ) ;
317+ console . info ( `Retrying ${ failedTests . size } failed test(s)` ) ;
318+ console . info ( '=' . repeat ( 60 ) ) ;
319+ console . info ( 'Failed tests:' ) ;
320+ failedTests . forEach ( ( failedTestName ) => console . info ( ` - ${ failedTestName } ` ) ) ;
321+ console . info ( '=' . repeat ( 60 ) ) ;
322+ console . info ( '\n' ) ;
323+ /* eslint-enable no-console */
324+
325+ const testsToRetry = new Set ( failedTests ) ;
326+ failedTests . clear ( ) ;
327+
328+ const retryRunner = createRunner ( true ) ;
329+ failedCount = await retry (
330+ ( ) => retryRunner . run ( runOptions ) ,
331+ LAUNCH_RETRY_ATTEMPTS ,
332+ ) ;
333+
334+ attemptsLeft -= 1 ;
335+
336+ /* eslint-disable no-console */
337+ console . info ( '\n' ) ;
338+ console . info ( '=' . repeat ( 60 ) ) ;
339+ console . info ( `ATTEMPT ${ attemptNumber } RESULTS` ) ;
340+ console . info ( '=' . repeat ( 60 ) ) ;
341+ console . info ( `Tests retried: ${ testsToRetry . size } ` ) ;
342+ console . info ( `Still failing: ${ failedCount } ` ) ;
343+ console . info ( `Passed on this attempt: ${ testsToRetry . size - failedCount } ` ) ;
344+ console . info ( '=' . repeat ( 60 ) ) ;
345+ console . info ( '\n' ) ;
346+ /* eslint-enable no-console */
347+
348+ if ( failedCount === 0 ) {
349+ /* eslint-disable no-console */
350+ console . info ( '✅ All previously failed tests now pass!' ) ;
351+ console . info ( '\n' ) ;
352+ /* eslint-enable no-console */
353+ break ;
354+ }
355+ }
356+
357+ /* eslint-disable no-console */
358+ console . info ( '\n' ) ;
359+ console . info ( '=' . repeat ( 60 ) ) ;
360+ console . info ( 'FINAL RETRY RESULTS' ) ;
361+ console . info ( '=' . repeat ( 60 ) ) ;
362+ console . info ( `Initially failed: ${ initialFailedCount } ` ) ;
363+ console . info ( `Total retry attempts used: ${ FAILED_TESTS_RETRY_ATTEMPTS - attemptsLeft } ` ) ;
364+ console . info ( `Final failing count: ${ failedCount } ` ) ;
365+ console . info ( `Successfully recovered: ${ initialFailedCount - failedCount } ` ) ;
366+ console . info ( '=' . repeat ( 60 ) ) ;
367+ console . info ( '\n' ) ;
368+ /* eslint-enable no-console */
369+ }
279370
280371 await testCafe . close ( ) ;
281372
0 commit comments