243 lines
46 KiB
JavaScript
243 lines
46 KiB
JavaScript
|
"use strict";
|
||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||
|
};
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
const lodash_1 = require("lodash");
|
||
|
const debug_1 = __importDefault(require("debug"));
|
||
|
const pretty_hrtime_1 = __importDefault(require("pretty-hrtime"));
|
||
|
const compiler_1 = __importDefault(require("../compiler"));
|
||
|
const connection_1 = __importDefault(require("../browser/connection"));
|
||
|
const browser_set_1 = __importDefault(require("./browser-set"));
|
||
|
const runtime_1 = require("../errors/runtime");
|
||
|
const types_1 = require("../errors/types");
|
||
|
const tested_app_1 = __importDefault(require("./tested-app"));
|
||
|
const parse_file_list_1 = __importDefault(require("../utils/parse-file-list"));
|
||
|
const load_1 = __importDefault(require("../custom-client-scripts/load"));
|
||
|
const string_1 = require("../utils/string");
|
||
|
const warning_log_1 = __importDefault(require("../notifications/warning-log"));
|
||
|
const warning_message_1 = __importDefault(require("../notifications/warning-message"));
|
||
|
const guard_time_execution_1 = __importDefault(require("../utils/guard-time-execution"));
|
||
|
const async_filter_1 = __importDefault(require("../utils/async-filter"));
|
||
|
const wrap_test_function_1 = __importDefault(require("../api/wrap-test-function"));
|
||
|
const type_assertions_1 = require("../errors/runtime/type-assertions");
|
||
|
const testcafe_hammerhead_1 = require("testcafe-hammerhead");
|
||
|
const assert_type_1 = __importDefault(require("../api/request-hooks/assert-type"));
|
||
|
const user_variables_1 = __importDefault(require("../api/user-variables"));
|
||
|
const option_names_1 = __importDefault(require("../configuration/option-names"));
|
||
|
const DEBUG_SCOPE = 'testcafe:bootstrapper';
|
||
|
function isPromiseError(value) {
|
||
|
return value.error !== void 0;
|
||
|
}
|
||
|
class Bootstrapper {
|
||
|
constructor({ browserConnectionGateway, compilerService, messageBus, configuration }) {
|
||
|
this.browserConnectionGateway = browserConnectionGateway;
|
||
|
this.concurrency = 1;
|
||
|
this.sources = [];
|
||
|
this.browsers = [];
|
||
|
this.reporters = [];
|
||
|
this.filter = void 0;
|
||
|
this.appCommand = void 0;
|
||
|
this.appInitDelay = void 0;
|
||
|
this.tsConfigPath = void 0;
|
||
|
this.clientScripts = [];
|
||
|
this.disableMultipleWindows = false;
|
||
|
this.proxyless = false;
|
||
|
this.compilerOptions = void 0;
|
||
|
this.debugLogger = (0, debug_1.default)(DEBUG_SCOPE);
|
||
|
this.warningLog = new warning_log_1.default(null, warning_log_1.default.createAddWarningCallback(messageBus));
|
||
|
this.compilerService = compilerService;
|
||
|
this.messageBus = messageBus;
|
||
|
this.configuration = configuration;
|
||
|
this.TESTS_COMPILATION_UPPERBOUND = 60;
|
||
|
}
|
||
|
static _getBrowserName(browser) {
|
||
|
if (browser instanceof connection_1.default)
|
||
|
return browser.browserInfo.browserName;
|
||
|
return browser.browserName;
|
||
|
}
|
||
|
static _splitBrowserInfo(browserInfo) {
|
||
|
const remotes = [];
|
||
|
const automated = [];
|
||
|
browserInfo.forEach(browser => {
|
||
|
if (browser instanceof connection_1.default)
|
||
|
remotes.push(browser);
|
||
|
else
|
||
|
automated.push(browser);
|
||
|
});
|
||
|
return { remotes, automated };
|
||
|
}
|
||
|
_createAutomatedConnections(browserInfo) {
|
||
|
if (!browserInfo)
|
||
|
return [];
|
||
|
return browserInfo
|
||
|
.map(browser => (0, lodash_1.times)(this.concurrency, () => new connection_1.default(this.browserConnectionGateway, Object.assign({}, browser), false, this.disableMultipleWindows, this.proxyless, this.messageBus)));
|
||
|
}
|
||
|
_getBrowserSetOptions() {
|
||
|
return {
|
||
|
concurrency: this.concurrency,
|
||
|
browserInitTimeout: this.browserInitTimeout,
|
||
|
warningLog: this.warningLog,
|
||
|
};
|
||
|
}
|
||
|
async _getBrowserConnections(browserInfo) {
|
||
|
const { automated, remotes } = Bootstrapper._splitBrowserInfo(browserInfo);
|
||
|
if (remotes && remotes.length % this.concurrency)
|
||
|
throw new runtime_1.GeneralError(types_1.RUNTIME_ERRORS.cannotDivideRemotesCountByConcurrency);
|
||
|
let browserConnections = this._createAutomatedConnections(automated);
|
||
|
remotes.forEach(remoteConnection => {
|
||
|
remoteConnection.messageBus = this.messageBus;
|
||
|
});
|
||
|
browserConnections = browserConnections.concat((0, lodash_1.chunk)(remotes, this.concurrency));
|
||
|
return browser_set_1.default.from(browserConnections, this._getBrowserSetOptions());
|
||
|
}
|
||
|
async _filterTests(tests, predicate) {
|
||
|
return (0, async_filter_1.default)(tests, test => {
|
||
|
const testFixture = test.fixture;
|
||
|
return predicate(test.name, testFixture.name, testFixture.path, test.meta, testFixture.meta);
|
||
|
});
|
||
|
}
|
||
|
async _compileTests({ sourceList, compilerOptions, runnableConfigurationId }) {
|
||
|
const baseUrl = this.configuration.getOption(option_names_1.default.baseUrl);
|
||
|
const experimentalEsm = this.configuration.getOption(option_names_1.default.experimentalEsm);
|
||
|
if (this.compilerService) {
|
||
|
await this.compilerService.init();
|
||
|
await this.compilerService.setUserVariables(user_variables_1.default.value);
|
||
|
return this.compilerService.getTests({ sourceList, compilerOptions, runnableConfigurationId }, baseUrl);
|
||
|
}
|
||
|
const compiler = new compiler_1.default(sourceList, compilerOptions, { baseUrl, isCompilerServiceMode: false, experimentalEsm });
|
||
|
return compiler.getTests();
|
||
|
}
|
||
|
_assertGlobalHooks() {
|
||
|
var _a, _b, _c, _d;
|
||
|
if (!this.hooks)
|
||
|
return;
|
||
|
if ((_a = this.hooks.fixture) === null || _a === void 0 ? void 0 : _a.before)
|
||
|
(0, type_assertions_1.assertType)(type_assertions_1.is.function, 'globalBefore', 'The fixture.globalBefore hook', this.hooks.fixture.before);
|
||
|
if ((_b = this.hooks.fixture) === null || _b === void 0 ? void 0 : _b.after)
|
||
|
(0, type_assertions_1.assertType)(type_assertions_1.is.function, 'globalAfter', 'The fixture.globalAfter hook', this.hooks.fixture.after);
|
||
|
if ((_c = this.hooks.test) === null || _c === void 0 ? void 0 : _c.before)
|
||
|
(0, type_assertions_1.assertType)(type_assertions_1.is.function, 'globalBefore', 'The test.globalBefore hook', this.hooks.test.before);
|
||
|
if ((_d = this.hooks.test) === null || _d === void 0 ? void 0 : _d.after)
|
||
|
(0, type_assertions_1.assertType)(type_assertions_1.is.function, 'globalAfter', 'The test.globalAfter hook', this.hooks.test.after);
|
||
|
if (this.hooks.request)
|
||
|
(0, assert_type_1.default)((0, lodash_1.flattenDeep)((0, lodash_1.castArray)(this.hooks.request)));
|
||
|
}
|
||
|
_setGlobalHooksToTests(tests) {
|
||
|
var _a, _b, _c, _d;
|
||
|
if (!this.hooks)
|
||
|
return;
|
||
|
this._assertGlobalHooks();
|
||
|
const fixtureBefore = ((_a = this.hooks.fixture) === null || _a === void 0 ? void 0 : _a.before) || null;
|
||
|
const fixtureAfter = ((_b = this.hooks.fixture) === null || _b === void 0 ? void 0 : _b.after) || null;
|
||
|
const testBefore = ((_c = this.hooks.test) === null || _c === void 0 ? void 0 : _c.before) ? (0, wrap_test_function_1.default)(this.hooks.test.before) : null;
|
||
|
const testAfter = ((_d = this.hooks.test) === null || _d === void 0 ? void 0 : _d.after) ? (0, wrap_test_function_1.default)(this.hooks.test.after) : null;
|
||
|
const request = this.hooks.request || [];
|
||
|
tests.forEach(item => {
|
||
|
if (item.fixture) {
|
||
|
item.fixture.globalBeforeFn = item.fixture.globalBeforeFn || fixtureBefore;
|
||
|
item.fixture.globalAfterFn = item.fixture.globalAfterFn || fixtureAfter;
|
||
|
}
|
||
|
item.globalBeforeFn = testBefore;
|
||
|
item.globalAfterFn = testAfter;
|
||
|
item.requestHooks = (0, lodash_1.union)((0, lodash_1.flattenDeep)((0, lodash_1.castArray)(request)), item.requestHooks);
|
||
|
});
|
||
|
}
|
||
|
async _getTests(id) {
|
||
|
const cwd = process.cwd();
|
||
|
const sourceList = await (0, parse_file_list_1.default)(this.sources, cwd);
|
||
|
if (!sourceList.length)
|
||
|
throw new runtime_1.GeneralError(types_1.RUNTIME_ERRORS.testFilesNotFound, cwd, (0, string_1.getConcatenatedValuesString)(this.sources, '\n', ''));
|
||
|
let tests = await (0, guard_time_execution_1.default)(async () => await this._compileTests({ sourceList, compilerOptions: this.compilerOptions, runnableConfigurationId: id }), elapsedTime => {
|
||
|
this.debugLogger(`tests compilation took ${(0, pretty_hrtime_1.default)(elapsedTime)}`);
|
||
|
const [elapsedSeconds] = elapsedTime;
|
||
|
if (elapsedSeconds > this.TESTS_COMPILATION_UPPERBOUND)
|
||
|
this.warningLog.addWarning(warning_message_1.default.testsCompilationTakesTooLong, (0, pretty_hrtime_1.default)(elapsedTime));
|
||
|
});
|
||
|
const testsWithOnlyFlag = tests.filter(test => test.only);
|
||
|
if (testsWithOnlyFlag.length)
|
||
|
tests = testsWithOnlyFlag;
|
||
|
if (!tests.length)
|
||
|
throw new runtime_1.GeneralError(types_1.RUNTIME_ERRORS.noTestsToRun);
|
||
|
if (this.filter)
|
||
|
tests = await this._filterTests(tests, this.filter);
|
||
|
if (!tests.length)
|
||
|
throw new runtime_1.GeneralError(types_1.RUNTIME_ERRORS.noTestsToRunDueFiltering);
|
||
|
this._setGlobalHooksToTests(tests);
|
||
|
return tests;
|
||
|
}
|
||
|
async _startTestedApp() {
|
||
|
if (!this.appCommand)
|
||
|
return void 0;
|
||
|
const testedApp = new tested_app_1.default();
|
||
|
await testedApp.start(this.appCommand, this.appInitDelay);
|
||
|
return testedApp;
|
||
|
}
|
||
|
async _canUseParallelBootstrapping(browserInfo) {
|
||
|
const isLocalPromises = browserInfo.map(browser => browser.provider.isLocalBrowser(void 0, Bootstrapper._getBrowserName(browser)));
|
||
|
const isLocalBrowsers = await Promise.all(isLocalPromises);
|
||
|
return isLocalBrowsers.every(result => result);
|
||
|
}
|
||
|
async _bootstrapSequence(browserInfo, id) {
|
||
|
const tests = await this._getTests(id);
|
||
|
const testedApp = await this._startTestedApp();
|
||
|
const browserSet = await this._getBrowserConnections(browserInfo);
|
||
|
return { tests, testedApp, browserSet };
|
||
|
}
|
||
|
_wrapBootstrappingPromise(promise) {
|
||
|
return promise
|
||
|
.then(result => ({ error: void 0, result }))
|
||
|
.catch(error => ({ result: void 0, error }));
|
||
|
}
|
||
|
async _getBootstrappingError(browserSetStatus, testsStatus, testedAppStatus) {
|
||
|
if (!isPromiseError(browserSetStatus))
|
||
|
await browserSetStatus.result.dispose();
|
||
|
if (!isPromiseError(browserSetStatus) && !isPromiseError(testedAppStatus) && testedAppStatus.result)
|
||
|
await testedAppStatus.result.kill();
|
||
|
if (isPromiseError(testsStatus))
|
||
|
return testsStatus.error;
|
||
|
if (isPromiseError(testedAppStatus))
|
||
|
return testedAppStatus.error;
|
||
|
if (isPromiseError(browserSetStatus))
|
||
|
return browserSetStatus.error;
|
||
|
return new Error('Unexpected call');
|
||
|
}
|
||
|
_getBootstrappingPromises(arg) {
|
||
|
const result = {};
|
||
|
for (const k in arg)
|
||
|
result[k] = this._wrapBootstrappingPromise(arg[k]);
|
||
|
return result;
|
||
|
}
|
||
|
async _bootstrapParallel(browserInfo, id) {
|
||
|
const bootstrappingPromises = {
|
||
|
browserSet: this._getBrowserConnections(browserInfo),
|
||
|
tests: this._getTests(id),
|
||
|
app: this._startTestedApp(),
|
||
|
};
|
||
|
const bootstrappingResultPromises = this._getBootstrappingPromises(bootstrappingPromises);
|
||
|
const bootstrappingResults = await Promise.all([
|
||
|
bootstrappingResultPromises.browserSet,
|
||
|
bootstrappingResultPromises.tests,
|
||
|
bootstrappingResultPromises.app,
|
||
|
]);
|
||
|
const [browserSetResults, testResults, appResults] = bootstrappingResults;
|
||
|
if (isPromiseError(browserSetResults) || isPromiseError(testResults) || isPromiseError(appResults))
|
||
|
throw await this._getBootstrappingError(...bootstrappingResults);
|
||
|
return {
|
||
|
browserSet: browserSetResults.result,
|
||
|
tests: testResults.result,
|
||
|
testedApp: appResults.result,
|
||
|
};
|
||
|
}
|
||
|
// API
|
||
|
async createRunnableConfiguration() {
|
||
|
const id = (0, testcafe_hammerhead_1.generateUniqueId)();
|
||
|
const commonClientScripts = await (0, load_1.default)(this.clientScripts);
|
||
|
if (await this._canUseParallelBootstrapping(this.browsers))
|
||
|
return Object.assign(Object.assign({}, await this._bootstrapParallel(this.browsers, id)), { commonClientScripts, id });
|
||
|
return Object.assign(Object.assign({}, await this._bootstrapSequence(this.browsers, id)), { commonClientScripts, id });
|
||
|
}
|
||
|
}
|
||
|
exports.default = Bootstrapper;
|
||
|
module.exports = exports.default;
|
||
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYm9vdHN0cmFwcGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3J1bm5lci9ib290c3RyYXBwZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSxtQ0FNZ0I7QUFFaEIsa0RBQTBCO0FBQzFCLGtFQUF1QztBQUN2QywyREFBbUM7QUFDbkMsdUVBQXVFO0FBQ3ZFLGdFQUF1QztBQUN2QywrQ0FBaUQ7QUFDakQsMkNBQWlEO0FBQ2pELDhEQUFxQztBQUNyQywrRUFBcUQ7QUFDckQseUVBQThEO0FBQzlELDRDQUE4RDtBQVM5RCwrRUFBc0Q7QUFDdEQsdUZBQWdFO0FBQ2hFLHlGQUErRDtBQUMvRCx5RUFBZ0Q7QUFHaEQsbUZBQXlEO0FBQ3pELHVFQUFtRTtBQUNuRSw2REFBdUQ7QUFDdkQsbUZBQXFFO0FBQ3JFLDJFQUFrRDtBQUVsRCxpRkFBeUQ7QUFFekQsTUFBTSxXQUFXLEdBQUcsdUJBQXVCLENBQUM7QUEyQjVDLFNBQVMsY0FBYyxDQUE4QixLQUEwQjtJQUMzRSxPQUFRLEtBQXlCLENBQUMsS0FBSyxLQUFLLEtBQUssQ0FBQyxDQUFDO0FBQ3ZELENBQUM7QUFhRCxNQUFxQixZQUFZO0lBeUI3QixZQUFvQixFQUFFLHdCQUF3QixFQUFFLGVBQWUsRUFBRSxVQUFVLEVBQUUsYUFBYSxFQUFvQjtRQUMxRyxJQUFJLENBQUMsd0JBQXdCLEdBQUcsd0JBQXdCLENBQUM7UUFDekQsSUFBSSxDQUFDLFdBQVcsR0FBZ0IsQ0FBQyxDQUFDO1FBQ2xDLElBQUksQ0FBQyxPQUFPLEdBQW9CLEVBQUUsQ0FBQztRQUNuQyxJQUFJLENBQUMsUUFBUSxHQUFtQixFQUFFLENBQUM7UUFDbkMsSUFBSSxDQUFDLFNBQVMsR0FBa0IsRUFBRSxDQUFDO1FBQ25DLElBQUksQ0FBQyxNQUFNLEdBQXFCLEtBQUssQ0FBQyxDQUFDO1FBQ3ZDLElBQUksQ0FBQyxVQUFVLEdBQWlCLEtBQUssQ0FBQyxDQUFDO1FBQ3ZDLElBQUksQ0FBQyxZ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
|