fix(test runner): do not show TimeoutError for unhandled rejection (#21971)
Unhandled error/rejection interrupts current test and produces a TimeoutError for it that should be ignored.
This commit is contained in:
		
							parent
							
								
									97d2c4a635
								
							
						
					
					
						commit
						120aaa777e
					
				|  | @ -53,6 +53,7 @@ export class TestInfoImpl implements TestInfo { | |||
|   readonly _traceEvents: trace.TraceEvent[] = []; | ||||
|   readonly _onTestFailureImmediateCallbacks = new Map<() => Promise<void>, string>(); // fn -> title
 | ||||
|   _didTimeout = false; | ||||
|   _wasInterrupted = false; | ||||
|   _lastStepId = 0; | ||||
| 
 | ||||
|   // ------------ TestInfo fields ------------
 | ||||
|  | @ -184,7 +185,7 @@ export class TestInfoImpl implements TestInfo { | |||
|     const timeoutError = await this._timeoutManager.runWithTimeout(cb); | ||||
|     // When interrupting, we arrive here with a timeoutError, but we should not
 | ||||
|     // consider it a timeout.
 | ||||
|     if (this.status !== 'interrupted' && timeoutError && !this._didTimeout) { | ||||
|     if (!this._wasInterrupted && timeoutError && !this._didTimeout) { | ||||
|       this._didTimeout = true; | ||||
|       this.errors.push(timeoutError); | ||||
|       // Do not overwrite existing failure upon hook/teardown timeout.
 | ||||
|  | @ -254,6 +255,15 @@ export class TestInfoImpl implements TestInfo { | |||
|     return step; | ||||
|   } | ||||
| 
 | ||||
|   _interrupt() { | ||||
|     // Mark as interrupted so we can ignore TimeoutError thrown by interrupt() call.
 | ||||
|     this._wasInterrupted = true; | ||||
|     this._timeoutManager.interrupt(); | ||||
|     // Do not overwrite existing failure (for example, unhandled rejection) with "interrupted".
 | ||||
|     if (this.status === 'passed') | ||||
|       this.status = 'interrupted'; | ||||
|   } | ||||
| 
 | ||||
|   _failWithError(error: TestInfoError, isHardError: boolean) { | ||||
|     // Do not overwrite any previous hard errors.
 | ||||
|     // Some (but not all) scenarios include:
 | ||||
|  |  | |||
|  | @ -99,12 +99,7 @@ export class WorkerMain extends ProcessRunner { | |||
|   private _stop(): Promise<void> { | ||||
|     if (!this._isStopped) { | ||||
|       this._isStopped = true; | ||||
| 
 | ||||
|       // Interrupt current action.
 | ||||
|       this._currentTest?._timeoutManager.interrupt(); | ||||
| 
 | ||||
|       if (this._currentTest && this._currentTest.status === 'passed') | ||||
|         this._currentTest.status = 'interrupted'; | ||||
|       this._currentTest?._interrupt(); | ||||
|     } | ||||
|     return this._runFinished; | ||||
|   } | ||||
|  |  | |||
|  | @ -394,6 +394,24 @@ test('test.{skip,fixme} should define a skipped test', async ({ runInlineTest }) | |||
|   expect(result.output).not.toContain('%%dontseethis'); | ||||
| }); | ||||
| 
 | ||||
| test('should report unhandled error during test and not report timeout', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.test.ts': ` | ||||
|       import { test, expect } from '@playwright/test'; | ||||
|       test('unhandled rejection', async () => { | ||||
|         setTimeout(() => { | ||||
|           throw new Error('Unhandled'); | ||||
|         }, 0); | ||||
|         await new Promise(f => setTimeout(f, 100)); | ||||
|       }); | ||||
|     `,
 | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(1); | ||||
|   expect(result.failed).toBe(1); | ||||
|   expect(result.output).toContain('Error: Unhandled'); | ||||
|   expect(result.output).not.toContain('Test timeout of 30000ms exceeded'); | ||||
| }); | ||||
| 
 | ||||
| test('should report unhandled rejection during worker shutdown', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.test.ts': ` | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue