diff --git a/packages/playwright-test/src/worker/testInfo.ts b/packages/playwright-test/src/worker/testInfo.ts index 7d0c689544..9626552a04 100644 --- a/packages/playwright-test/src/worker/testInfo.ts +++ b/packages/playwright-test/src/worker/testInfo.ts @@ -53,6 +53,7 @@ export class TestInfoImpl implements TestInfo { readonly _traceEvents: trace.TraceEvent[] = []; readonly _onTestFailureImmediateCallbacks = new Map<() => Promise, 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: diff --git a/packages/playwright-test/src/worker/workerMain.ts b/packages/playwright-test/src/worker/workerMain.ts index 32944326f2..0c2fcc9015 100644 --- a/packages/playwright-test/src/worker/workerMain.ts +++ b/packages/playwright-test/src/worker/workerMain.ts @@ -99,12 +99,7 @@ export class WorkerMain extends ProcessRunner { private _stop(): Promise { 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; } diff --git a/tests/playwright-test/basic.spec.ts b/tests/playwright-test/basic.spec.ts index 2565c1a9d6..cd56a0fd2d 100644 --- a/tests/playwright-test/basic.spec.ts +++ b/tests/playwright-test/basic.spec.ts @@ -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': `