279 lines
10 KiB
JavaScript
279 lines
10 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
Object.defineProperty(exports, "__esModule", {
|
||
|
value: true
|
||
|
});
|
||
|
exports.FixtureRunner = void 0;
|
||
|
var _util = require("../util");
|
||
|
var _utils = require("playwright-core/lib/utils");
|
||
|
var _fixtures = require("../common/fixtures");
|
||
|
/**
|
||
|
* 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 Fixture {
|
||
|
constructor(runner, registration) {
|
||
|
this.runner = void 0;
|
||
|
this.registration = void 0;
|
||
|
this.value = void 0;
|
||
|
this.failed = false;
|
||
|
this._useFuncFinished = void 0;
|
||
|
this._selfTeardownComplete = void 0;
|
||
|
this._setupDescription = void 0;
|
||
|
this._teardownDescription = void 0;
|
||
|
this._stepInfo = void 0;
|
||
|
this._deps = new Set();
|
||
|
this._usages = new Set();
|
||
|
this.runner = runner;
|
||
|
this.registration = registration;
|
||
|
this.value = null;
|
||
|
const shouldGenerateStep = !this.registration.box && !this.registration.option;
|
||
|
const isUserFixture = this.registration.location && (0, _util.filterStackFile)(this.registration.location.file);
|
||
|
const title = this.registration.customTitle || this.registration.name;
|
||
|
const location = isUserFixture ? this.registration.location : undefined;
|
||
|
this._stepInfo = shouldGenerateStep ? {
|
||
|
category: 'fixture',
|
||
|
location
|
||
|
} : undefined;
|
||
|
this._setupDescription = {
|
||
|
title,
|
||
|
phase: 'setup',
|
||
|
location,
|
||
|
slot: this.registration.timeout === undefined ? undefined : {
|
||
|
timeout: this.registration.timeout,
|
||
|
elapsed: 0
|
||
|
}
|
||
|
};
|
||
|
this._teardownDescription = {
|
||
|
...this._setupDescription,
|
||
|
phase: 'teardown'
|
||
|
};
|
||
|
}
|
||
|
async setup(testInfo, runnable) {
|
||
|
this.runner.instanceForId.set(this.registration.id, this);
|
||
|
if (typeof this.registration.fn !== 'function') {
|
||
|
this.value = this.registration.fn;
|
||
|
return;
|
||
|
}
|
||
|
await testInfo._runAsStage({
|
||
|
title: `fixture: ${this.registration.name}`,
|
||
|
runnable: {
|
||
|
...runnable,
|
||
|
fixture: this._setupDescription
|
||
|
},
|
||
|
stepInfo: this._stepInfo
|
||
|
}, async () => {
|
||
|
await this._setupInternal(testInfo);
|
||
|
});
|
||
|
}
|
||
|
async _setupInternal(testInfo) {
|
||
|
const params = {};
|
||
|
for (const name of this.registration.deps) {
|
||
|
const registration = this.runner.pool.resolve(name, this.registration);
|
||
|
const dep = this.runner.instanceForId.get(registration.id);
|
||
|
if (!dep) {
|
||
|
this.failed = true;
|
||
|
return;
|
||
|
}
|
||
|
// Fixture teardown is root => leaves, when we need to teardown a fixture,
|
||
|
// it recursively tears down its usages first.
|
||
|
dep._usages.add(this);
|
||
|
// Don't forget to decrement all usages when fixture goes.
|
||
|
// Otherwise worker-scope fixtures will retain test-scope fixtures forever.
|
||
|
this._deps.add(dep);
|
||
|
params[name] = dep.value;
|
||
|
if (dep.failed) {
|
||
|
this.failed = true;
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
let called = false;
|
||
|
const useFuncStarted = new _utils.ManualPromise();
|
||
|
const useFunc = async value => {
|
||
|
if (called) throw new Error(`Cannot provide fixture value for the second time`);
|
||
|
called = true;
|
||
|
this.value = value;
|
||
|
this._useFuncFinished = new _utils.ManualPromise();
|
||
|
useFuncStarted.resolve();
|
||
|
await this._useFuncFinished;
|
||
|
};
|
||
|
const workerInfo = {
|
||
|
config: testInfo.config,
|
||
|
parallelIndex: testInfo.parallelIndex,
|
||
|
workerIndex: testInfo.workerIndex,
|
||
|
project: testInfo.project
|
||
|
};
|
||
|
const info = this.registration.scope === 'worker' ? workerInfo : testInfo;
|
||
|
this._selfTeardownComplete = (async () => {
|
||
|
try {
|
||
|
await this.registration.fn(params, useFunc, info);
|
||
|
} catch (error) {
|
||
|
this.failed = true;
|
||
|
if (!useFuncStarted.isDone()) useFuncStarted.reject(error);else throw error;
|
||
|
}
|
||
|
})();
|
||
|
await useFuncStarted;
|
||
|
}
|
||
|
async teardown(testInfo, runnable) {
|
||
|
try {
|
||
|
const fixtureRunnable = {
|
||
|
...runnable,
|
||
|
fixture: this._teardownDescription
|
||
|
};
|
||
|
// Do not even start the teardown for a fixture that does not have any
|
||
|
// time remaining in the time slot. This avoids cascading timeouts.
|
||
|
if (!testInfo._timeoutManager.isTimeExhaustedFor(fixtureRunnable)) {
|
||
|
await testInfo._runAsStage({
|
||
|
title: `fixture: ${this.registration.name}`,
|
||
|
runnable: fixtureRunnable,
|
||
|
stepInfo: this._stepInfo
|
||
|
}, async () => {
|
||
|
await this._teardownInternal();
|
||
|
});
|
||
|
}
|
||
|
} finally {
|
||
|
// To preserve fixtures integrity, forcefully cleanup fixtures
|
||
|
// that cannnot teardown due to a timeout or an error.
|
||
|
for (const dep of this._deps) dep._usages.delete(this);
|
||
|
this.runner.instanceForId.delete(this.registration.id);
|
||
|
}
|
||
|
}
|
||
|
async _teardownInternal() {
|
||
|
if (typeof this.registration.fn !== 'function') return;
|
||
|
if (this._usages.size !== 0) {
|
||
|
// TODO: replace with assert.
|
||
|
console.error('Internal error: fixture integrity at', this._teardownDescription.title); // eslint-disable-line no-console
|
||
|
this._usages.clear();
|
||
|
}
|
||
|
if (this._useFuncFinished) {
|
||
|
this._useFuncFinished.resolve();
|
||
|
this._useFuncFinished = undefined;
|
||
|
await this._selfTeardownComplete;
|
||
|
}
|
||
|
}
|
||
|
_collectFixturesInTeardownOrder(scope, collector) {
|
||
|
if (this.registration.scope !== scope) return;
|
||
|
for (const fixture of this._usages) fixture._collectFixturesInTeardownOrder(scope, collector);
|
||
|
collector.add(this);
|
||
|
}
|
||
|
}
|
||
|
class FixtureRunner {
|
||
|
constructor() {
|
||
|
this.testScopeClean = true;
|
||
|
this.pool = void 0;
|
||
|
this.instanceForId = new Map();
|
||
|
}
|
||
|
setPool(pool) {
|
||
|
if (!this.testScopeClean) throw new Error('Did not teardown test scope');
|
||
|
if (this.pool && pool.digest !== this.pool.digest) {
|
||
|
throw new Error([`Playwright detected inconsistent test.use() options.`, `Most common mistakes that lead to this issue:`, ` - Calling test.use() outside of the test file, for example in a common helper.`, ` - One test file imports from another test file.`].join('\n'));
|
||
|
}
|
||
|
this.pool = pool;
|
||
|
}
|
||
|
_collectFixturesInSetupOrder(registration, collector) {
|
||
|
if (collector.has(registration)) return;
|
||
|
for (const name of registration.deps) {
|
||
|
const dep = this.pool.resolve(name, registration);
|
||
|
this._collectFixturesInSetupOrder(dep, collector);
|
||
|
}
|
||
|
collector.add(registration);
|
||
|
}
|
||
|
async teardownScope(scope, testInfo, runnable) {
|
||
|
// Teardown fixtures in the reverse order.
|
||
|
const fixtures = Array.from(this.instanceForId.values()).reverse();
|
||
|
const collector = new Set();
|
||
|
for (const fixture of fixtures) fixture._collectFixturesInTeardownOrder(scope, collector);
|
||
|
let firstError;
|
||
|
for (const fixture of collector) {
|
||
|
try {
|
||
|
await fixture.teardown(testInfo, runnable);
|
||
|
} catch (error) {
|
||
|
var _firstError;
|
||
|
firstError = (_firstError = firstError) !== null && _firstError !== void 0 ? _firstError : error;
|
||
|
}
|
||
|
}
|
||
|
if (scope === 'test') this.testScopeClean = true;
|
||
|
if (firstError) throw firstError;
|
||
|
}
|
||
|
async resolveParametersForFunction(fn, testInfo, autoFixtures, runnable) {
|
||
|
const collector = new Set();
|
||
|
|
||
|
// Collect automatic fixtures.
|
||
|
const auto = [];
|
||
|
for (const registration of this.pool.autoFixtures()) {
|
||
|
let shouldRun = true;
|
||
|
if (autoFixtures === 'all-hooks-only') shouldRun = registration.scope === 'worker' || registration.auto === 'all-hooks-included';else if (autoFixtures === 'worker') shouldRun = registration.scope === 'worker';
|
||
|
if (shouldRun) auto.push(registration);
|
||
|
}
|
||
|
auto.sort((r1, r2) => (r1.scope === 'worker' ? 0 : 1) - (r2.scope === 'worker' ? 0 : 1));
|
||
|
for (const registration of auto) this._collectFixturesInSetupOrder(registration, collector);
|
||
|
|
||
|
// Collect used fixtures.
|
||
|
const names = getRequiredFixtureNames(fn);
|
||
|
for (const name of names) this._collectFixturesInSetupOrder(this.pool.resolve(name), collector);
|
||
|
|
||
|
// Setup fixtures.
|
||
|
for (const registration of collector) await this._setupFixtureForRegistration(registration, testInfo, runnable);
|
||
|
|
||
|
// Create params object.
|
||
|
const params = {};
|
||
|
for (const name of names) {
|
||
|
const registration = this.pool.resolve(name);
|
||
|
const fixture = this.instanceForId.get(registration.id);
|
||
|
if (!fixture || fixture.failed) return null;
|
||
|
params[name] = fixture.value;
|
||
|
}
|
||
|
return params;
|
||
|
}
|
||
|
async resolveParametersAndRunFunction(fn, testInfo, autoFixtures, runnable) {
|
||
|
const params = await this.resolveParametersForFunction(fn, testInfo, autoFixtures, runnable);
|
||
|
if (params === null) {
|
||
|
// Do not run the function when fixture setup has already failed.
|
||
|
return null;
|
||
|
}
|
||
|
await testInfo._runAsStage({
|
||
|
title: 'run function',
|
||
|
runnable
|
||
|
}, async () => {
|
||
|
await fn(params, testInfo);
|
||
|
});
|
||
|
}
|
||
|
async _setupFixtureForRegistration(registration, testInfo, runnable) {
|
||
|
if (registration.scope === 'test') this.testScopeClean = false;
|
||
|
let fixture = this.instanceForId.get(registration.id);
|
||
|
if (fixture) return fixture;
|
||
|
fixture = new Fixture(this, registration);
|
||
|
await fixture.setup(testInfo, runnable);
|
||
|
return fixture;
|
||
|
}
|
||
|
dependsOnWorkerFixturesOnly(fn, location) {
|
||
|
const names = getRequiredFixtureNames(fn, location);
|
||
|
for (const name of names) {
|
||
|
const registration = this.pool.resolve(name);
|
||
|
if (registration.scope !== 'worker') return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
exports.FixtureRunner = FixtureRunner;
|
||
|
function getRequiredFixtureNames(fn, location) {
|
||
|
return (0, _fixtures.fixtureParameterNames)(fn, location !== null && location !== void 0 ? location : {
|
||
|
file: '<unknown>',
|
||
|
line: 1,
|
||
|
column: 1
|
||
|
}, e => {
|
||
|
throw new Error(`${(0, _util.formatLocation)(e.location)}: ${e.message}`);
|
||
|
});
|
||
|
}
|