620 lines
28 KiB
JavaScript
620 lines
28 KiB
JavaScript
"use strict";
|
||
|
||
Object.defineProperty(exports, "__esModule", {
|
||
value: true
|
||
});
|
||
exports.create = exports.WorkerMain = void 0;
|
||
var _utils = require("playwright-core/lib/utils");
|
||
var _configLoader = require("../common/configLoader");
|
||
var _globals = require("../common/globals");
|
||
var _ipc = require("../common/ipc");
|
||
var _util = require("../util");
|
||
var _fixtureRunner = require("./fixtureRunner");
|
||
var _testInfo = require("./testInfo");
|
||
var _util2 = require("./util");
|
||
var _fixtures = require("../common/fixtures");
|
||
var _poolBuilder = require("../common/poolBuilder");
|
||
var _process = require("../common/process");
|
||
var _suiteUtils = require("../common/suiteUtils");
|
||
var _testLoader = require("../common/testLoader");
|
||
/**
|
||
* Copyright Microsoft Corporation. All rights reserved.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
class WorkerMain extends _process.ProcessRunner {
|
||
constructor(params) {
|
||
super();
|
||
this._params = void 0;
|
||
this._config = void 0;
|
||
this._project = void 0;
|
||
this._poolBuilder = void 0;
|
||
this._fixtureRunner = void 0;
|
||
// Accumulated fatal errors that cannot be attributed to a test.
|
||
this._fatalErrors = [];
|
||
// Whether we should skip running remaining tests in this suite because
|
||
// of a setup error, usually beforeAll hook.
|
||
this._skipRemainingTestsInSuite = void 0;
|
||
// The stage of the full cleanup. Once "finished", we can safely stop running anything.
|
||
this._didRunFullCleanup = false;
|
||
// Whether the worker was requested to stop.
|
||
this._isStopped = false;
|
||
// This promise resolves once the single "run test group" call finishes.
|
||
this._runFinished = new _utils.ManualPromise();
|
||
this._currentTest = null;
|
||
this._lastRunningTests = [];
|
||
this._totalRunningTests = 0;
|
||
// Suites that had their beforeAll hooks, but not afterAll hooks executed.
|
||
// These suites still need afterAll hooks to be executed for the proper cleanup.
|
||
// Contains dynamic annotations originated by modifiers with a callback, e.g. `test.skip(() => true)`.
|
||
this._activeSuites = new Map();
|
||
process.env.TEST_WORKER_INDEX = String(params.workerIndex);
|
||
process.env.TEST_PARALLEL_INDEX = String(params.parallelIndex);
|
||
(0, _globals.setIsWorkerProcess)();
|
||
this._params = params;
|
||
this._fixtureRunner = new _fixtureRunner.FixtureRunner();
|
||
|
||
// Resolve this promise, so worker does not stall waiting for the non-existent run to finish,
|
||
// when it was sopped before running any test group.
|
||
this._runFinished.resolve();
|
||
process.on('unhandledRejection', reason => this.unhandledError(reason));
|
||
process.on('uncaughtException', error => this.unhandledError(error));
|
||
process.stdout.write = (chunk, cb) => {
|
||
var _this$_currentTest;
|
||
this.dispatchEvent('stdOut', (0, _ipc.stdioChunkToParams)(chunk));
|
||
(_this$_currentTest = this._currentTest) === null || _this$_currentTest === void 0 || _this$_currentTest._tracing.appendStdioToTrace('stdout', chunk);
|
||
if (typeof cb === 'function') process.nextTick(cb);
|
||
return true;
|
||
};
|
||
if (!process.env.PW_RUNNER_DEBUG) {
|
||
process.stderr.write = (chunk, cb) => {
|
||
var _this$_currentTest2;
|
||
this.dispatchEvent('stdErr', (0, _ipc.stdioChunkToParams)(chunk));
|
||
(_this$_currentTest2 = this._currentTest) === null || _this$_currentTest2 === void 0 || _this$_currentTest2._tracing.appendStdioToTrace('stderr', chunk);
|
||
if (typeof cb === 'function') process.nextTick(cb);
|
||
return true;
|
||
};
|
||
}
|
||
}
|
||
_stop() {
|
||
if (!this._isStopped) {
|
||
var _this$_currentTest3;
|
||
this._isStopped = true;
|
||
(_this$_currentTest3 = this._currentTest) === null || _this$_currentTest3 === void 0 || _this$_currentTest3._interrupt();
|
||
}
|
||
return this._runFinished;
|
||
}
|
||
async gracefullyClose() {
|
||
try {
|
||
await this._stop();
|
||
if (!this._config) {
|
||
// We never set anything up and we can crash on attempting cleanup
|
||
return;
|
||
}
|
||
// Ignore top-level errors, they are already inside TestInfo.errors.
|
||
const fakeTestInfo = new _testInfo.TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {});
|
||
const runnable = {
|
||
type: 'teardown'
|
||
};
|
||
// We have to load the project to get the right deadline below.
|
||
await fakeTestInfo._runWithTimeout(runnable, () => this._loadIfNeeded()).catch(() => {});
|
||
await this._fixtureRunner.teardownScope('test', fakeTestInfo, runnable).catch(() => {});
|
||
await this._fixtureRunner.teardownScope('worker', fakeTestInfo, runnable).catch(() => {});
|
||
// Close any other browsers launched in this process. This includes anything launched
|
||
// manually in the test/hooks and internal browsers like Playwright Inspector.
|
||
await fakeTestInfo._runWithTimeout(runnable, () => (0, _utils.gracefullyCloseAll)()).catch(() => {});
|
||
this._fatalErrors.push(...fakeTestInfo.errors);
|
||
} catch (e) {
|
||
this._fatalErrors.push((0, _util2.testInfoError)(e));
|
||
}
|
||
if (this._fatalErrors.length) {
|
||
this._appendProcessTeardownDiagnostics(this._fatalErrors[this._fatalErrors.length - 1]);
|
||
const payload = {
|
||
fatalErrors: this._fatalErrors
|
||
};
|
||
this.dispatchEvent('teardownErrors', payload);
|
||
}
|
||
}
|
||
_appendProcessTeardownDiagnostics(error) {
|
||
if (!this._lastRunningTests.length) return;
|
||
const count = this._totalRunningTests === 1 ? '1 test' : `${this._totalRunningTests} tests`;
|
||
let lastMessage = '';
|
||
if (this._lastRunningTests.length < this._totalRunningTests) lastMessage = `, last ${this._lastRunningTests.length} tests were`;
|
||
const message = ['', '', _utils.colors.red(`Failed worker ran ${count}${lastMessage}:`), ...this._lastRunningTests.map(test => formatTestTitle(test, this._project.project.name))].join('\n');
|
||
if (error.message) {
|
||
if (error.stack) {
|
||
let index = error.stack.indexOf(error.message);
|
||
if (index !== -1) {
|
||
index += error.message.length;
|
||
error.stack = error.stack.substring(0, index) + message + error.stack.substring(index);
|
||
}
|
||
}
|
||
error.message += message;
|
||
} else if (error.value) {
|
||
error.value += message;
|
||
}
|
||
}
|
||
unhandledError(error) {
|
||
// No current test - fatal error.
|
||
if (!this._currentTest) {
|
||
if (!this._fatalErrors.length) this._fatalErrors.push((0, _util2.testInfoError)(error));
|
||
void this._stop();
|
||
return;
|
||
}
|
||
|
||
// We do not differentiate between errors in the control flow
|
||
// and unhandled errors - both lead to the test failing. This is good for regular tests,
|
||
// so that you can, e.g. expect() from inside an event handler. The test fails,
|
||
// and we restart the worker.
|
||
if (!this._currentTest._hasUnhandledError) {
|
||
this._currentTest._hasUnhandledError = true;
|
||
this._currentTest._failWithError(error);
|
||
}
|
||
|
||
// For tests marked with test.fail(), this might be a problem when unhandled error
|
||
// is not coming from the user test code (legit failure), but from fixtures or test runner.
|
||
//
|
||
// Ideally, we would mark this test as "failed unexpectedly" and show that in the report.
|
||
// However, we do not have such a special test status, so the test will be considered ok (failed as expected).
|
||
//
|
||
// To avoid messing up future tests, we forcefully stop the worker, unless it is
|
||
// an expect() error which we know does not mess things up.
|
||
const isExpectError = error instanceof Error && !!error.matcherResult;
|
||
const shouldContinueInThisWorker = this._currentTest.expectedStatus === 'failed' && isExpectError;
|
||
if (!shouldContinueInThisWorker) void this._stop();
|
||
}
|
||
async _loadIfNeeded() {
|
||
if (this._config) return;
|
||
const config = await (0, _configLoader.deserializeConfig)(this._params.config);
|
||
const project = config.projects.find(p => p.id === this._params.projectId);
|
||
if (!project) throw new Error(`Project "${this._params.projectId}" not found in the worker process. Make sure project name does not change.`);
|
||
this._config = config;
|
||
this._project = project;
|
||
this._poolBuilder = _poolBuilder.PoolBuilder.createForWorker(this._project);
|
||
}
|
||
async runTestGroup(runPayload) {
|
||
this._runFinished = new _utils.ManualPromise();
|
||
const entries = new Map(runPayload.entries.map(e => [e.testId, e]));
|
||
let fatalUnknownTestIds;
|
||
try {
|
||
await this._loadIfNeeded();
|
||
const fileSuite = await (0, _testLoader.loadTestFile)(runPayload.file, this._config.config.rootDir);
|
||
const suite = (0, _suiteUtils.bindFileSuiteToProject)(this._project, fileSuite);
|
||
if (this._params.repeatEachIndex) (0, _suiteUtils.applyRepeatEachIndex)(this._project, suite, this._params.repeatEachIndex);
|
||
const hasEntries = (0, _suiteUtils.filterTestsRemoveEmptySuites)(suite, test => entries.has(test.id));
|
||
if (hasEntries) {
|
||
this._poolBuilder.buildPools(suite);
|
||
this._activeSuites = new Map();
|
||
this._didRunFullCleanup = false;
|
||
const tests = suite.allTests();
|
||
for (let i = 0; i < tests.length; i++) {
|
||
// Do not run tests after full cleanup, because we are entirely done.
|
||
if (this._isStopped && this._didRunFullCleanup) break;
|
||
const entry = entries.get(tests[i].id);
|
||
entries.delete(tests[i].id);
|
||
(0, _util.debugTest)(`test started "${tests[i].title}"`);
|
||
await this._runTest(tests[i], entry.retry, tests[i + 1]);
|
||
(0, _util.debugTest)(`test finished "${tests[i].title}"`);
|
||
}
|
||
} else {
|
||
fatalUnknownTestIds = runPayload.entries.map(e => e.testId);
|
||
void this._stop();
|
||
}
|
||
} catch (e) {
|
||
// In theory, we should run above code without any errors.
|
||
// However, in the case we screwed up, or loadTestFile failed in the worker
|
||
// but not in the runner, let's do a fatal error.
|
||
this._fatalErrors.push((0, _util2.testInfoError)(e));
|
||
void this._stop();
|
||
} finally {
|
||
const donePayload = {
|
||
fatalErrors: this._fatalErrors,
|
||
skipTestsDueToSetupFailure: [],
|
||
fatalUnknownTestIds
|
||
};
|
||
for (const test of ((_this$_skipRemainingT = this._skipRemainingTestsInSuite) === null || _this$_skipRemainingT === void 0 ? void 0 : _this$_skipRemainingT.allTests()) || []) {
|
||
var _this$_skipRemainingT;
|
||
if (entries.has(test.id)) donePayload.skipTestsDueToSetupFailure.push(test.id);
|
||
}
|
||
this.dispatchEvent('done', donePayload);
|
||
this._fatalErrors = [];
|
||
this._skipRemainingTestsInSuite = undefined;
|
||
this._runFinished.resolve();
|
||
}
|
||
}
|
||
async _runTest(test, retry, nextTest) {
|
||
const testInfo = new _testInfo.TestInfoImpl(this._config, this._project, this._params, test, retry, stepBeginPayload => this.dispatchEvent('stepBegin', stepBeginPayload), stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload), attachment => this.dispatchEvent('attach', attachment));
|
||
const processAnnotation = annotation => {
|
||
testInfo.annotations.push(annotation);
|
||
switch (annotation.type) {
|
||
case 'fixme':
|
||
case 'skip':
|
||
testInfo.expectedStatus = 'skipped';
|
||
break;
|
||
case 'fail':
|
||
if (testInfo.expectedStatus !== 'skipped') testInfo.expectedStatus = 'failed';
|
||
break;
|
||
case 'slow':
|
||
testInfo._timeoutManager.slow();
|
||
break;
|
||
}
|
||
};
|
||
if (!this._isStopped) this._fixtureRunner.setPool(test._pool);
|
||
const suites = getSuites(test);
|
||
const reversedSuites = suites.slice().reverse();
|
||
const nextSuites = new Set(getSuites(nextTest));
|
||
testInfo._timeoutManager.setTimeout(test.timeout);
|
||
for (const annotation of test.annotations) processAnnotation(annotation);
|
||
|
||
// Process existing annotations dynamically set for parent suites.
|
||
for (const suite of suites) {
|
||
const extraAnnotations = this._activeSuites.get(suite) || [];
|
||
for (const annotation of extraAnnotations) processAnnotation(annotation);
|
||
}
|
||
this._currentTest = testInfo;
|
||
(0, _globals.setCurrentTestInfo)(testInfo);
|
||
this.dispatchEvent('testBegin', buildTestBeginPayload(testInfo));
|
||
const isSkipped = testInfo.expectedStatus === 'skipped';
|
||
const hasAfterAllToRunBeforeNextTest = reversedSuites.some(suite => {
|
||
return this._activeSuites.has(suite) && !nextSuites.has(suite) && suite._hooks.some(hook => hook.type === 'afterAll');
|
||
});
|
||
if (isSkipped && nextTest && !hasAfterAllToRunBeforeNextTest) {
|
||
// Fast path - this test is skipped, and there are more tests that will handle cleanup.
|
||
testInfo.status = 'skipped';
|
||
this.dispatchEvent('testEnd', buildTestEndPayload(testInfo));
|
||
return;
|
||
}
|
||
this._totalRunningTests++;
|
||
this._lastRunningTests.push(test);
|
||
if (this._lastRunningTests.length > 10) this._lastRunningTests.shift();
|
||
let shouldRunAfterEachHooks = false;
|
||
testInfo._allowSkips = true;
|
||
|
||
// Create warning if any of the async calls were not awaited in various stages.
|
||
const checkForFloatingPromises = functionDescription => {
|
||
if (process.env.PW_DISABLE_FLOATING_PROMISES_WARNING) return;
|
||
if (!testInfo._floatingPromiseScope.hasFloatingPromises()) return;
|
||
// TODO: 1.52: Actually build annotations
|
||
// testInfo.annotations.push({ type: 'warning', description: `Some async calls were not awaited by the end of ${functionDescription}. This can cause flakiness.` });
|
||
testInfo._floatingPromiseScope.clear();
|
||
};
|
||
await (async () => {
|
||
await testInfo._runWithTimeout({
|
||
type: 'test'
|
||
}, async () => {
|
||
// Ideally, "trace" would be an config-level option belonging to the
|
||
// test runner instead of a fixture belonging to Playwright.
|
||
// However, for backwards compatibility, we have to read it from a fixture today.
|
||
// We decided to not introduce the config-level option just yet.
|
||
const traceFixtureRegistration = test._pool.resolve('trace');
|
||
if (!traceFixtureRegistration) return;
|
||
if (typeof traceFixtureRegistration.fn === 'function') throw new Error(`"trace" option cannot be a function`);
|
||
await testInfo._tracing.startIfNeeded(traceFixtureRegistration.fn);
|
||
});
|
||
if (this._isStopped || isSkipped) {
|
||
// Two reasons to get here:
|
||
// - Last test is skipped, so we should not run the test, but run the cleanup.
|
||
// - Worker is requested to stop, but was not able to run full cleanup yet.
|
||
// We should skip the test, but run the cleanup.
|
||
testInfo.status = 'skipped';
|
||
return;
|
||
}
|
||
await (0, _utils.removeFolders)([testInfo.outputDir]);
|
||
let testFunctionParams = null;
|
||
await testInfo._runAsStep({
|
||
title: 'Before Hooks',
|
||
category: 'hook'
|
||
}, async () => {
|
||
// Run "beforeAll" hooks, unless already run during previous tests.
|
||
for (const suite of suites) await this._runBeforeAllHooksForSuite(suite, testInfo);
|
||
|
||
// Run "beforeEach" hooks. Once started with "beforeEach", we must run all "afterEach" hooks as well.
|
||
shouldRunAfterEachHooks = true;
|
||
await this._runEachHooksForSuites(suites, 'beforeEach', testInfo);
|
||
|
||
// Setup fixtures required by the test.
|
||
testFunctionParams = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo, 'test', {
|
||
type: 'test'
|
||
});
|
||
});
|
||
checkForFloatingPromises('beforeAll/beforeEach hooks');
|
||
if (testFunctionParams === null) {
|
||
// Fixture setup failed or was skipped, we should not run the test now.
|
||
return;
|
||
}
|
||
await testInfo._runWithTimeout({
|
||
type: 'test'
|
||
}, async () => {
|
||
// Now run the test itself.
|
||
const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
|
||
await fn(testFunctionParams, testInfo);
|
||
checkForFloatingPromises('the test');
|
||
});
|
||
})().catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
||
|
||
// Update duration, so it is available in fixture teardown and afterEach hooks.
|
||
testInfo.duration = testInfo._timeoutManager.defaultSlot().elapsed | 0;
|
||
|
||
// No skips in after hooks.
|
||
testInfo._allowSkips = true;
|
||
|
||
// After hooks get an additional timeout.
|
||
const afterHooksTimeout = calculateMaxTimeout(this._project.project.timeout, testInfo.timeout);
|
||
const afterHooksSlot = {
|
||
timeout: afterHooksTimeout,
|
||
elapsed: 0
|
||
};
|
||
await testInfo._runAsStep({
|
||
title: 'After Hooks',
|
||
category: 'hook'
|
||
}, async () => {
|
||
let firstAfterHooksError;
|
||
try {
|
||
// Run "immediately upon test function finish" callback.
|
||
await testInfo._runWithTimeout({
|
||
type: 'test',
|
||
slot: afterHooksSlot
|
||
}, async () => {
|
||
var _testInfo$_onDidFinis;
|
||
return (_testInfo$_onDidFinis = testInfo._onDidFinishTestFunction) === null || _testInfo$_onDidFinis === void 0 ? void 0 : _testInfo$_onDidFinis.call(testInfo);
|
||
});
|
||
} catch (error) {
|
||
firstAfterHooksError = firstAfterHooksError !== null && firstAfterHooksError !== void 0 ? firstAfterHooksError : error;
|
||
}
|
||
try {
|
||
// Run "afterEach" hooks, unless we failed at beforeAll stage.
|
||
if (shouldRunAfterEachHooks) await this._runEachHooksForSuites(reversedSuites, 'afterEach', testInfo, afterHooksSlot);
|
||
} catch (error) {
|
||
firstAfterHooksError = firstAfterHooksError !== null && firstAfterHooksError !== void 0 ? firstAfterHooksError : error;
|
||
}
|
||
testInfo._tracing.didFinishTestFunctionAndAfterEachHooks();
|
||
try {
|
||
// Teardown test-scoped fixtures. Attribute to 'test' so that users understand
|
||
// they should probably increase the test timeout to fix this issue.
|
||
await this._fixtureRunner.teardownScope('test', testInfo, {
|
||
type: 'test',
|
||
slot: afterHooksSlot
|
||
});
|
||
} catch (error) {
|
||
firstAfterHooksError = firstAfterHooksError !== null && firstAfterHooksError !== void 0 ? firstAfterHooksError : error;
|
||
}
|
||
|
||
// Run "afterAll" hooks for suites that are not shared with the next test.
|
||
// In case of failure the worker will be stopped and we have to make sure that afterAll
|
||
// hooks run before worker fixtures teardown.
|
||
for (const suite of reversedSuites) {
|
||
if (!nextSuites.has(suite) || testInfo._isFailure()) {
|
||
try {
|
||
await this._runAfterAllHooksForSuite(suite, testInfo);
|
||
} catch (error) {
|
||
// Continue running "afterAll" hooks even after some of them timeout.
|
||
firstAfterHooksError = firstAfterHooksError !== null && firstAfterHooksError !== void 0 ? firstAfterHooksError : error;
|
||
}
|
||
}
|
||
}
|
||
if (firstAfterHooksError) throw firstAfterHooksError;
|
||
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
||
|
||
checkForFloatingPromises('afterAll/afterEach hooks');
|
||
if (testInfo._isFailure()) this._isStopped = true;
|
||
if (this._isStopped) {
|
||
// Run all remaining "afterAll" hooks and teardown all fixtures when worker is shutting down.
|
||
// Mark as "cleaned up" early to avoid running cleanup twice.
|
||
this._didRunFullCleanup = true;
|
||
await testInfo._runAsStep({
|
||
title: 'Worker Cleanup',
|
||
category: 'hook'
|
||
}, async () => {
|
||
let firstWorkerCleanupError;
|
||
|
||
// Give it more time for the full cleanup.
|
||
const teardownSlot = {
|
||
timeout: this._project.project.timeout,
|
||
elapsed: 0
|
||
};
|
||
try {
|
||
// Attribute to 'test' so that users understand they should probably increate the test timeout to fix this issue.
|
||
await this._fixtureRunner.teardownScope('test', testInfo, {
|
||
type: 'test',
|
||
slot: teardownSlot
|
||
});
|
||
} catch (error) {
|
||
firstWorkerCleanupError = firstWorkerCleanupError !== null && firstWorkerCleanupError !== void 0 ? firstWorkerCleanupError : error;
|
||
}
|
||
for (const suite of reversedSuites) {
|
||
try {
|
||
await this._runAfterAllHooksForSuite(suite, testInfo);
|
||
} catch (error) {
|
||
firstWorkerCleanupError = firstWorkerCleanupError !== null && firstWorkerCleanupError !== void 0 ? firstWorkerCleanupError : error;
|
||
}
|
||
}
|
||
try {
|
||
// Attribute to 'teardown' because worker fixtures are not perceived as a part of a test.
|
||
await this._fixtureRunner.teardownScope('worker', testInfo, {
|
||
type: 'teardown',
|
||
slot: teardownSlot
|
||
});
|
||
} catch (error) {
|
||
firstWorkerCleanupError = firstWorkerCleanupError !== null && firstWorkerCleanupError !== void 0 ? firstWorkerCleanupError : error;
|
||
}
|
||
if (firstWorkerCleanupError) throw firstWorkerCleanupError;
|
||
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
||
}
|
||
const tracingSlot = {
|
||
timeout: this._project.project.timeout,
|
||
elapsed: 0
|
||
};
|
||
await testInfo._runWithTimeout({
|
||
type: 'test',
|
||
slot: tracingSlot
|
||
}, async () => {
|
||
await testInfo._tracing.stopIfNeeded();
|
||
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
||
|
||
testInfo.duration = testInfo._timeoutManager.defaultSlot().elapsed + afterHooksSlot.elapsed | 0;
|
||
this._currentTest = null;
|
||
(0, _globals.setCurrentTestInfo)(null);
|
||
this.dispatchEvent('testEnd', buildTestEndPayload(testInfo));
|
||
const preserveOutput = this._config.config.preserveOutput === 'always' || this._config.config.preserveOutput === 'failures-only' && testInfo._isFailure();
|
||
if (!preserveOutput) await (0, _utils.removeFolders)([testInfo.outputDir]);
|
||
}
|
||
_collectHooksAndModifiers(suite, type, testInfo) {
|
||
const runnables = [];
|
||
for (const modifier of suite._modifiers) {
|
||
const modifierType = this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location) ? 'beforeAll' : 'beforeEach';
|
||
if (modifierType !== type) continue;
|
||
const fn = async fixtures => {
|
||
const result = await modifier.fn(fixtures);
|
||
testInfo[modifier.type](!!result, modifier.description);
|
||
};
|
||
(0, _fixtures.inheritFixtureNames)(modifier.fn, fn);
|
||
runnables.push({
|
||
title: `${modifier.type} modifier`,
|
||
location: modifier.location,
|
||
type: modifier.type,
|
||
fn
|
||
});
|
||
}
|
||
// Modifiers first, then hooks.
|
||
runnables.push(...suite._hooks.filter(hook => hook.type === type));
|
||
return runnables;
|
||
}
|
||
async _runBeforeAllHooksForSuite(suite, testInfo) {
|
||
if (this._activeSuites.has(suite)) return;
|
||
const extraAnnotations = [];
|
||
this._activeSuites.set(suite, extraAnnotations);
|
||
await this._runAllHooksForSuite(suite, testInfo, 'beforeAll', extraAnnotations);
|
||
}
|
||
async _runAllHooksForSuite(suite, testInfo, type, extraAnnotations) {
|
||
// Always run all the hooks, and capture the first error.
|
||
let firstError;
|
||
for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) {
|
||
try {
|
||
await testInfo._runAsStep({
|
||
title: hook.title,
|
||
category: 'hook',
|
||
location: hook.location
|
||
}, async () => {
|
||
// Separate time slot for each beforeAll/afterAll hook.
|
||
const timeSlot = {
|
||
timeout: this._project.project.timeout,
|
||
elapsed: 0
|
||
};
|
||
const runnable = {
|
||
type: hook.type,
|
||
slot: timeSlot,
|
||
location: hook.location
|
||
};
|
||
const existingAnnotations = new Set(testInfo.annotations);
|
||
try {
|
||
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only', runnable);
|
||
} finally {
|
||
if (extraAnnotations) {
|
||
// Inherit all annotations defined in the beforeAll/modifer to all tests in the suite.
|
||
const newAnnotations = testInfo.annotations.filter(a => !existingAnnotations.has(a));
|
||
extraAnnotations.push(...newAnnotations);
|
||
}
|
||
// Each beforeAll/afterAll hook has its own scope for test fixtures. Attribute to the same runnable and timeSlot.
|
||
// Note: we must teardown even after hook fails, because we'll run more hooks.
|
||
await this._fixtureRunner.teardownScope('test', testInfo, runnable);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
firstError = firstError !== null && firstError !== void 0 ? firstError : error;
|
||
// Skip in beforeAll/modifier prevents others from running.
|
||
if (type === 'beforeAll' && error instanceof _testInfo.TestSkipError) break;
|
||
if (type === 'beforeAll' && !this._skipRemainingTestsInSuite) {
|
||
// This will inform dispatcher that we should not run more tests from this group
|
||
// because we had a beforeAll error.
|
||
// This behavior avoids getting the same common error for each test.
|
||
this._skipRemainingTestsInSuite = suite;
|
||
}
|
||
}
|
||
}
|
||
if (firstError) throw firstError;
|
||
}
|
||
async _runAfterAllHooksForSuite(suite, testInfo) {
|
||
if (!this._activeSuites.has(suite)) return;
|
||
this._activeSuites.delete(suite);
|
||
await this._runAllHooksForSuite(suite, testInfo, 'afterAll');
|
||
}
|
||
async _runEachHooksForSuites(suites, type, testInfo, slot) {
|
||
// Always run all the hooks, unless one of the times out, and capture the first error.
|
||
let firstError;
|
||
const hooks = suites.map(suite => this._collectHooksAndModifiers(suite, type, testInfo)).flat();
|
||
for (const hook of hooks) {
|
||
const runnable = {
|
||
type: hook.type,
|
||
location: hook.location,
|
||
slot
|
||
};
|
||
if (testInfo._timeoutManager.isTimeExhaustedFor(runnable)) {
|
||
// Do not run hooks that will timeout right away.
|
||
continue;
|
||
}
|
||
try {
|
||
await testInfo._runAsStep({
|
||
title: hook.title,
|
||
category: 'hook',
|
||
location: hook.location
|
||
}, async () => {
|
||
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'test', runnable);
|
||
});
|
||
} catch (error) {
|
||
firstError = firstError !== null && firstError !== void 0 ? firstError : error;
|
||
// Skip in modifier prevents others from running.
|
||
if (error instanceof _testInfo.TestSkipError) break;
|
||
}
|
||
}
|
||
if (firstError) throw firstError;
|
||
}
|
||
}
|
||
exports.WorkerMain = WorkerMain;
|
||
function buildTestBeginPayload(testInfo) {
|
||
return {
|
||
testId: testInfo.testId,
|
||
startWallTime: testInfo._startWallTime
|
||
};
|
||
}
|
||
function buildTestEndPayload(testInfo) {
|
||
return {
|
||
testId: testInfo.testId,
|
||
duration: testInfo.duration,
|
||
status: testInfo.status,
|
||
errors: testInfo.errors,
|
||
hasNonRetriableError: testInfo._hasNonRetriableError,
|
||
expectedStatus: testInfo.expectedStatus,
|
||
annotations: testInfo.annotations,
|
||
timeout: testInfo.timeout
|
||
};
|
||
}
|
||
function getSuites(test) {
|
||
const suites = [];
|
||
for (let suite = test === null || test === void 0 ? void 0 : test.parent; suite; suite = suite.parent) suites.push(suite);
|
||
suites.reverse(); // Put root suite first.
|
||
return suites;
|
||
}
|
||
function formatTestTitle(test, projectName) {
|
||
// file, ...describes, test
|
||
const [, ...titles] = test.titlePath();
|
||
const location = `${(0, _util.relativeFilePath)(test.location.file)}:${test.location.line}:${test.location.column}`;
|
||
const projectTitle = projectName ? `[${projectName}] › ` : '';
|
||
return `${projectTitle}${location} › ${titles.join(' › ')}`;
|
||
}
|
||
function calculateMaxTimeout(t1, t2) {
|
||
// Zero means "no timeout".
|
||
return !t1 || !t2 ? 0 : Math.max(t1, t2);
|
||
}
|
||
const create = params => new WorkerMain(params);
|
||
exports.create = create; |