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:
Dmitry Gozman 2023-03-24 15:03:49 -07:00 committed by GitHub
parent 97d2c4a635
commit 120aaa777e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 30 additions and 7 deletions

View File

@ -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:

View File

@ -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;
}

View File

@ -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': `