folio
Version:
A customizable test framework to build your own test frameworks. Foundation for the [Playwright test runner](https://github.com/microsoft/playwright-test).
291 lines • 12.7 kB
JavaScript
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.FixturePool = exports.assignConfig = exports.config = exports.setParameterValues = exports.matrix = exports.assignParameters = exports.parameters = exports.parameterRegistrations = exports.currentTestInfo = exports.setCurrentTestInfo = void 0;
const debug_1 = require("./debug");
const util_1 = require("./util");
let currentTestInfoValue = null;
function setCurrentTestInfo(testInfo) {
currentTestInfoValue = testInfo;
}
exports.setCurrentTestInfo = setCurrentTestInfo;
function currentTestInfo() {
return currentTestInfoValue;
}
exports.currentTestInfo = currentTestInfo;
exports.parameterRegistrations = new Map();
exports.parameters = {};
function assignParameters(params) {
exports.parameters = Object.assign(exports.parameters, params);
}
exports.assignParameters = assignParameters;
exports.matrix = {};
function setParameterValues(name, values) {
if (!(name in exports.matrix))
throw util_1.errorWithCallLocation(`Unregistered parameter '${name}' was set.`);
exports.matrix[name] = values;
}
exports.setParameterValues = setParameterValues;
exports.config = {};
function assignConfig(c) {
exports.config = Object.assign(exports.config, c);
}
exports.assignConfig = assignConfig;
class Fixture {
constructor(pool, registration) {
this._setup = false;
this._teardown = false;
this.pool = pool;
this.registration = registration;
this.usages = new Set();
this.hasGeneratorValue = registration.name in exports.parameters;
this.value = this.hasGeneratorValue ? exports.parameters[registration.name] : null;
if (this.hasGeneratorValue && this.registration.deps.length)
throw util_1.errorWithCallLocation(`Parameter fixture "${this.registration.name}" should not have dependencies`);
}
async setup() {
if (this.hasGeneratorValue)
return;
const params = {};
for (const name of this.registration.deps) {
const registration = this.pool._resolveDependency(this.registration, name);
if (!registration)
throw util_1.errorWithCallLocation(`Unknown fixture "${name}"`);
const dep = await this.pool.setupFixtureForRegistration(registration);
dep.usages.add(this);
params[name] = dep.value;
}
let setupFenceFulfill;
let setupFenceReject;
let called = false;
const setupFence = new Promise((f, r) => { setupFenceFulfill = f; setupFenceReject = r; });
const teardownFence = new Promise(f => this._teardownFenceCallback = f);
debug_1.debugLog(`setup fixture "${this.registration.name}"`);
this._tearDownComplete = this.registration.fn(params, async (value) => {
if (called)
throw util_1.errorWithCallLocation(`Cannot provide fixture value for the second time`);
called = true;
this.value = value;
setupFenceFulfill();
return await teardownFence;
}).catch((e) => {
if (!this._setup)
setupFenceReject(e);
else
throw e;
});
await setupFence;
this._setup = true;
}
async teardown() {
if (this.hasGeneratorValue) {
this.pool.instances.delete(this.registration);
return;
}
if (this._teardown)
return;
this._teardown = true;
for (const fixture of this.usages)
await fixture.teardown();
this.usages.clear();
if (this._setup) {
debug_1.debugLog(`teardown fixture "${this.registration.name}"`);
this._teardownFenceCallback();
await this._tearDownComplete;
}
this.pool.instances.delete(this.registration);
}
}
let lastFixturePoolId = 0;
class FixturePool {
constructor(parentPool) {
this.instances = new Map();
this.parentPool = parentPool;
this.id = ++lastFixturePoolId;
this.registrations = new Map(parentPool ? parentPool.registrations : []);
}
union(other) {
const containsRegistrationRecursively = (pool, registration) => {
if (pool.registrations.get(registration.name) === registration)
return true;
if (!pool.parentPool)
return false;
return containsRegistrationRecursively(pool.parentPool, registration);
};
const result = new FixturePool(this);
for (const [name, otherRegistration] of other.registrations) {
const thisRegistration = this.registrations.get(name);
if (!thisRegistration) {
result.registrations.set(name, otherRegistration);
}
else if (containsRegistrationRecursively(this, otherRegistration)) {
// |this| contains an override - do nothing.
}
else if (containsRegistrationRecursively(other, thisRegistration)) {
// |other| contains an override - use it.
result.registrations.set(name, otherRegistration);
}
else {
// Both |this| and |other| have a different override - throw.
throw util_1.errorWithCallLocation(`Fixture "${name}" is defined in both fixture sets.`);
}
}
return result;
}
registerFixture(name, scope, fn, auto) {
const previous = this.registrations.get(name);
if (previous) {
if (previous.scope !== scope)
throw util_1.errorWithCallLocation(`Fixture "${name}" has already been registered as a { scope: '${previous.scope}' } fixture. Use a different name for this ${scope} fixture.`);
else
throw util_1.errorWithCallLocation(`Fixture "${name}" has already been registered. Use 'override' to override it in a specific test file.`);
}
const deps = fixtureParameterNames(fn);
const registration = { name, scope, fn, auto, isOverride: false, deps, super: previous };
this.registrations.set(name, registration);
}
overrideFixture(name, fn) {
const previous = this.registrations.get(name);
if (!previous)
throw util_1.errorWithCallLocation(`Fixture "${name}" has not been registered yet. Use 'init' instead.`);
const deps = fixtureParameterNames(fn);
const registration = { name, scope: previous.scope, fn, auto: previous.auto, isOverride: true, deps, super: previous };
this.registrations.set(name, registration);
}
registerWorkerParameter(parameter) {
if (exports.parameterRegistrations.has(parameter.name))
throw util_1.errorWithCallLocation(`Parameter "${parameter.name}" has been already registered`);
exports.parameterRegistrations.set(parameter.name, parameter);
exports.matrix[parameter.name] = [parameter.defaultValue];
}
validate() {
const markers = new Map();
const stack = [];
const visit = (registration) => {
markers.set(registration, 'visiting');
stack.push(registration);
for (const name of registration.deps) {
const dep = this._resolveDependency(registration, name);
if (!dep)
throw util_1.errorWithCallLocation(`Fixture "${registration.name}" has unknown parameter "${name}".`);
if (registration.scope === 'worker' && dep.scope === 'test')
throw util_1.errorWithCallLocation(`Worker fixture "${registration.name}" cannot depend on a test fixture "${name}".`);
if (!markers.has(dep)) {
visit(dep);
}
else if (markers.get(dep) === 'visiting') {
const index = stack.indexOf(dep);
const names = stack.slice(index, stack.length).map(r => `"${r.name}"`);
throw util_1.errorWithCallLocation(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle.`);
}
}
markers.set(registration, 'visited');
stack.pop();
};
for (const registration of this.registrations.values())
visit(registration);
}
parametersForFunction(fn, prefix, allowTestFixtures) {
const result = new Set();
const visit = (registration) => {
if (exports.parameterRegistrations.has(registration.name))
result.add(registration.name);
for (const name of registration.deps)
visit(this._resolveDependency(registration, name));
};
for (const name of fixtureParameterNames(fn)) {
const registration = this.registrations.get(name);
if (!registration)
throw util_1.errorWithCallLocation(`${prefix} has unknown parameter "${name}".`);
if (!allowTestFixtures && registration.scope === 'test')
throw util_1.errorWithCallLocation(`${prefix} cannot depend on a test fixture "${name}".`);
visit(registration);
}
for (const registration of this.registrations.values()) {
if (registration.auto) {
if (!allowTestFixtures && registration.scope === 'test')
throw util_1.errorWithCallLocation(`${prefix} cannot depend on a test fixture "${registration.name}".`);
visit(registration);
}
}
return Array.from(result);
}
async setupFixture(name) {
const registration = this.registrations.get(name);
if (!registration)
throw util_1.errorWithCallLocation('Unknown fixture: ' + name);
return this.setupFixtureForRegistration(registration);
}
async setupFixtureForRegistration(registration) {
let fixture = this.instances.get(registration);
if (fixture)
return fixture;
fixture = new Fixture(this, registration);
this.instances.set(registration, fixture);
await fixture.setup();
return fixture;
}
async teardownScope(scope) {
for (const [, fixture] of this.instances) {
if (fixture.registration.scope === scope)
await fixture.teardown();
}
}
async resolveParametersAndRunHookOrTest(fn) {
// Install all automatic fixtures.
for (const registration of this.registrations.values()) {
if (registration.auto)
await this.setupFixtureForRegistration(registration);
}
// Install used fixtures.
const names = fixtureParameterNames(fn);
const params = {};
for (const name of names) {
const fixture = await this.setupFixture(name);
params[name] = fixture.value;
}
return fn(params);
}
_resolveDependency(registration, name) {
if (name === registration.name)
return registration.super;
return this.registrations.get(name);
}
}
exports.FixturePool = FixturePool;
const signatureSymbol = Symbol('signature');
function fixtureParameterNames(fn) {
if (!fn[signatureSymbol])
fn[signatureSymbol] = innerFixtureParameterNames(fn);
return fn[signatureSymbol];
}
function innerFixtureParameterNames(fn) {
const text = fn.toString();
const match = text.match(/(?:async)?(?:\s+function)?[^\(]*\(([^})]*)/);
if (!match)
return [];
const trimmedParams = match[1].trim();
if (!trimmedParams)
return [];
if (trimmedParams && trimmedParams[0] !== '{')
throw util_1.errorWithCallLocation('First argument must use the object destructuring pattern: ' + trimmedParams);
const signature = trimmedParams.substring(1).trim();
if (!signature)
return [];
return signature.split(',').map((t) => t.trim().split(':')[0].trim());
}
//# sourceMappingURL=fixtures.js.map