fix(test runner): do not swallow afterEach errors after skipped tests (#16276)
Drive-by: add more test runner logging.
This commit is contained in:
parent
97fa2518e9
commit
919b1cbb22
|
|
@ -167,7 +167,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
async _runWithTimeout(cb: () => Promise<any>): Promise<void> {
|
async _runWithTimeout(cb: () => Promise<any>): Promise<void> {
|
||||||
const timeoutError = await this._timeoutManager.runWithTimeout(cb);
|
const timeoutError = await this._timeoutManager.runWithTimeout(cb);
|
||||||
// Do not overwrite existing failure upon hook/teardown timeout.
|
// Do not overwrite existing failure upon hook/teardown timeout.
|
||||||
if (timeoutError && this.status === 'passed') {
|
if (timeoutError && (this.status === 'passed' || this.status === 'skipped')) {
|
||||||
this.status = 'timedOut';
|
this.status = 'timedOut';
|
||||||
this.errors.push(timeoutError);
|
this.errors.push(timeoutError);
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +202,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
return;
|
return;
|
||||||
if (isHardError)
|
if (isHardError)
|
||||||
this._hasHardError = true;
|
this._hasHardError = true;
|
||||||
if (this.status === 'passed')
|
if (this.status === 'passed' || this.status === 'skipped')
|
||||||
this.status = 'failed';
|
this.status = 'failed';
|
||||||
this.errors.push(error);
|
this.errors.push(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import { colors, rimraf } from 'playwright-core/lib/utilsBundle';
|
import { colors, rimraf } from 'playwright-core/lib/utilsBundle';
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { relativeFilePath, serializeError } from './util';
|
import { debugTest, formatLocation, relativeFilePath, serializeError } from './util';
|
||||||
import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, WatchTestResolvedPayload } from './ipc';
|
import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, WatchTestResolvedPayload } from './ipc';
|
||||||
import { setCurrentTestInfo } from './globals';
|
import { setCurrentTestInfo } from './globals';
|
||||||
import { Loader } from './loader';
|
import { Loader } from './loader';
|
||||||
|
|
@ -195,7 +195,9 @@ export class WorkerRunner extends EventEmitter {
|
||||||
break;
|
break;
|
||||||
const entry = entries.get(tests[i].id)!;
|
const entry = entries.get(tests[i].id)!;
|
||||||
entries.delete(tests[i].id);
|
entries.delete(tests[i].id);
|
||||||
|
debugTest(`test started "${tests[i].title}"`);
|
||||||
await this._runTest(tests[i], entry.retry, tests[i + 1]);
|
await this._runTest(tests[i], entry.retry, tests[i + 1]);
|
||||||
|
debugTest(`test finished "${tests[i].title}"`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fatalUnknownTestIds = runPayload.entries.map(e => e.testId);
|
fatalUnknownTestIds = runPayload.entries.map(e => e.testId);
|
||||||
|
|
@ -384,8 +386,10 @@ export class WorkerRunner extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now run the test itself.
|
// Now run the test itself.
|
||||||
|
debugTest(`test function started`);
|
||||||
const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
|
const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
|
||||||
await fn(params, testInfo);
|
await fn(params, testInfo);
|
||||||
|
debugTest(`test function finished`);
|
||||||
}, 'allowSkips');
|
}, 'allowSkips');
|
||||||
|
|
||||||
beforeHooksStep.complete({ error: maybeError }); // Second complete is a no-op.
|
beforeHooksStep.complete({ error: maybeError }); // Second complete is a no-op.
|
||||||
|
|
@ -498,6 +502,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
const actualScope = this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location) ? 'worker' : 'test';
|
const actualScope = this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location) ? 'worker' : 'test';
|
||||||
if (actualScope !== scope)
|
if (actualScope !== scope)
|
||||||
continue;
|
continue;
|
||||||
|
debugTest(`modifier at "${formatLocation(modifier.location)}" started`);
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: modifier.type, location: modifier.location, slot: timeSlot });
|
testInfo._timeoutManager.setCurrentRunnable({ type: modifier.type, location: modifier.location, slot: timeSlot });
|
||||||
const result = await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(modifier.fn, testInfo, scope), {
|
const result = await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(modifier.fn, testInfo, scope), {
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
|
|
@ -506,6 +511,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
forceNoParent: false,
|
forceNoParent: false,
|
||||||
location: modifier.location,
|
location: modifier.location,
|
||||||
});
|
});
|
||||||
|
debugTest(`modifier at "${formatLocation(modifier.location)}" finished`);
|
||||||
if (result && extraAnnotations)
|
if (result && extraAnnotations)
|
||||||
extraAnnotations.push({ type: modifier.type, description: modifier.description });
|
extraAnnotations.push({ type: modifier.type, description: modifier.description });
|
||||||
testInfo[modifier.type](!!result, modifier.description);
|
testInfo[modifier.type](!!result, modifier.description);
|
||||||
|
|
@ -520,6 +526,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
for (const hook of suite._hooks) {
|
for (const hook of suite._hooks) {
|
||||||
if (hook.type !== 'beforeAll')
|
if (hook.type !== 'beforeAll')
|
||||||
continue;
|
continue;
|
||||||
|
debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" started`);
|
||||||
try {
|
try {
|
||||||
// Separate time slot for each "beforeAll" hook.
|
// Separate time slot for each "beforeAll" hook.
|
||||||
const timeSlot = { timeout: this._project.timeout, elapsed: 0 };
|
const timeSlot = { timeout: this._project.timeout, elapsed: 0 };
|
||||||
|
|
@ -535,6 +542,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
// Always run all the hooks, and capture the first error.
|
// Always run all the hooks, and capture the first error.
|
||||||
beforeAllError = beforeAllError || e;
|
beforeAllError = beforeAllError || e;
|
||||||
}
|
}
|
||||||
|
debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" finished`);
|
||||||
}
|
}
|
||||||
if (beforeAllError)
|
if (beforeAllError)
|
||||||
throw beforeAllError;
|
throw beforeAllError;
|
||||||
|
|
@ -548,6 +556,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
for (const hook of suite._hooks) {
|
for (const hook of suite._hooks) {
|
||||||
if (hook.type !== 'afterAll')
|
if (hook.type !== 'afterAll')
|
||||||
continue;
|
continue;
|
||||||
|
debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" started`);
|
||||||
const afterAllError = await testInfo._runFn(async () => {
|
const afterAllError = await testInfo._runFn(async () => {
|
||||||
// Separate time slot for each "afterAll" hook.
|
// Separate time slot for each "afterAll" hook.
|
||||||
const timeSlot = { timeout: this._project.timeout, elapsed: 0 };
|
const timeSlot = { timeout: this._project.timeout, elapsed: 0 };
|
||||||
|
|
@ -561,6 +570,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
firstError = firstError || afterAllError;
|
firstError = firstError || afterAllError;
|
||||||
|
debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" finished`);
|
||||||
}
|
}
|
||||||
return firstError;
|
return firstError;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -813,3 +813,33 @@ test('afterAll should run if last test was skipped 2', async ({ runInlineTest })
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.output).toContain('after-all');
|
expect(result.output).toContain('after-all');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('afterEach timeout after skipped test should be reported', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test.afterEach(async () => {
|
||||||
|
await new Promise(() => {});
|
||||||
|
});
|
||||||
|
test('skipped', () => { test.skip(); });
|
||||||
|
`,
|
||||||
|
}, { timeout: 2000 });
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.output).toContain('Test timeout of 2000ms exceeded while running "afterEach" hook.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('afterEach exception after skipped test should be reported', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test.afterEach(async () => {
|
||||||
|
throw new Error('oh my!');
|
||||||
|
});
|
||||||
|
test('skipped', () => { test.skip(); });
|
||||||
|
`,
|
||||||
|
}, { timeout: 2000 });
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.output).toContain('Error: oh my!');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue