Innovenergy_trunk/frontend/node_modules/testcafe/lib/browser/connection/index.js

420 lines
66 KiB
JavaScript
Raw Normal View History

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const debug_1 = __importDefault(require("debug"));
const time_limit_promise_1 = __importDefault(require("time-limit-promise"));
const events_1 = require("events");
const mustache_1 = __importDefault(require("mustache"));
const lodash_1 = require("lodash");
const parse_user_agent_1 = require("../../utils/parse-user-agent");
const read_file_relative_1 = require("read-file-relative");
const promisify_event_1 = __importDefault(require("promisify-event"));
const nanoid_1 = require("nanoid");
const command_1 = __importDefault(require("./command"));
const status_1 = __importDefault(require("./status"));
const heartbeat_status_1 = __importDefault(require("./heartbeat-status"));
const runtime_1 = require("../../errors/runtime");
const types_1 = require("../../errors/types");
const warning_log_1 = __importDefault(require("../../notifications/warning-log"));
const service_routes_1 = __importDefault(require("./service-routes"));
const browser_connection_timeouts_1 = require("../../utils/browser-connection-timeouts");
const tracker_1 = __importDefault(require("./tracker"));
const getBrowserConnectionDebugScope = (id) => `testcafe:browser:connection:${id}`;
const IDLE_PAGE_TEMPLATE = (0, read_file_relative_1.readSync)('../../client/browser/idle-page/index.html.mustache');
class BrowserConnection extends events_1.EventEmitter {
constructor(gateway, browserInfo, permanent, disableMultipleWindows = false, proxyless = false, messageBus) {
super();
this._currentTestRun = null;
this.url = '';
this.idleUrl = '';
this.forcedIdleUrl = '';
this.initScriptUrl = '';
this.heartbeatUrl = '';
this.statusUrl = '';
this.activeWindowIdUrl = '';
this.closeWindowUrl = '';
this.statusDoneUrl = '';
this.heartbeatRelativeUrl = '';
this.statusRelativeUrl = '';
this.statusDoneRelativeUrl = '';
this.idleRelativeUrl = '';
this.openFileProtocolRelativeUrl = '';
this.openFileProtocolUrl = '';
this.dispatchProxylessEventRelativeUrl = '';
this.osInfo = null;
this.HEARTBEAT_TIMEOUT = browser_connection_timeouts_1.HEARTBEAT_TIMEOUT;
this.BROWSER_CLOSE_TIMEOUT = browser_connection_timeouts_1.BROWSER_CLOSE_TIMEOUT;
this.BROWSER_RESTART_TIMEOUT = browser_connection_timeouts_1.BROWSER_RESTART_TIMEOUT;
this.id = BrowserConnection._generateId();
this.jobQueue = [];
this.initScriptsQueue = [];
this.browserConnectionGateway = gateway;
this.disconnectionPromise = null;
this.testRunAborted = false;
this.warningLog = new warning_log_1.default(null, warning_log_1.default.createAddWarningCallback(messageBus));
this.debugLogger = (0, debug_1.default)(getBrowserConnectionDebugScope(this.id));
if (messageBus)
this.messageBus = messageBus;
this.browserInfo = browserInfo;
this.browserInfo.userAgentProviderMetaInfo = '';
this.provider = browserInfo.provider;
this.permanent = permanent;
this.status = status_1.default.uninitialized;
this.idle = true;
this.heartbeatTimeout = null;
this.pendingTestRunInfo = null;
this.disableMultipleWindows = disableMultipleWindows;
this.proxyless = proxyless;
this._buildCommunicationUrls(gateway.proxy);
this._setEventHandlers();
tracker_1.default.add(this);
this.previousActiveWindowId = null;
this.browserConnectionGateway.startServingConnection(this);
// NOTE: Give a caller time to assign event listeners
process.nextTick(() => this._runBrowser());
}
_buildCommunicationUrls(proxy) {
this.url = proxy.resolveRelativeServiceUrl(`${service_routes_1.default.connect}/${this.id}`);
this.forcedIdleUrl = proxy.resolveRelativeServiceUrl(`${service_routes_1.default.idleForced}/${this.id}`);
this.initScriptUrl = proxy.resolveRelativeServiceUrl(`${service_routes_1.default.initScript}/${this.id}`);
this.heartbeatRelativeUrl = `${service_routes_1.default.heartbeat}/${this.id}`;
this.statusRelativeUrl = `${service_routes_1.default.status}/${this.id}`;
this.statusDoneRelativeUrl = `${service_routes_1.default.statusDone}/${this.id}`;
this.idleRelativeUrl = `${service_routes_1.default.idle}/${this.id}`;
this.activeWindowIdUrl = `${service_routes_1.default.activeWindowId}/${this.id}`;
this.closeWindowUrl = `${service_routes_1.default.closeWindow}/${this.id}`;
this.openFileProtocolRelativeUrl = `${service_routes_1.default.openFileProtocol}/${this.id}`;
this.dispatchProxylessEventRelativeUrl = `${service_routes_1.default.dispatchProxylessEvent}/${this.id}`;
this.idleUrl = proxy.resolveRelativeServiceUrl(this.idleRelativeUrl);
this.heartbeatUrl = proxy.resolveRelativeServiceUrl(this.heartbeatRelativeUrl);
this.statusUrl = proxy.resolveRelativeServiceUrl(this.statusRelativeUrl);
this.statusDoneUrl = proxy.resolveRelativeServiceUrl(this.statusDoneRelativeUrl);
this.openFileProtocolUrl = proxy.resolveRelativeServiceUrl(this.openFileProtocolRelativeUrl);
}
set messageBus(messageBus) {
this._messageBus = messageBus;
this.warningLog.callback = warning_log_1.default.createAddWarningCallback(this._messageBus);
if (messageBus) {
messageBus.on('test-run-start', testRun => {
if (testRun.browserConnection.id === this.id)
this._currentTestRun = testRun;
});
}
this.emit('message-bus-initialized', messageBus);
}
get messageBus() {
return this._messageBus;
}
_setEventHandlers() {
this.on('error', e => {
this.debugLogger(e);
this._forceIdle();
this.close();
});
for (const name in status_1.default) {
const status = status_1.default[name];
this.on(status, () => {
this.debugLogger(`status changed to '${status}'`);
});
}
}
static _generateId() {
return (0, nanoid_1.nanoid)(7);
}
_getAdditionalBrowserOptions() {
const options = {
disableMultipleWindows: this.disableMultipleWindows,
};
if (this.proxyless) {
options.proxyless = {
serviceDomains: [
this.browserConnectionGateway.proxy.server1Info.domain,
this.browserConnectionGateway.proxy.server2Info.domain,
],
developmentMode: this.browserConnectionGateway.proxy.options.developmentMode,
};
}
return options;
}
async _runBrowser() {
try {
const additionalOptions = this._getAdditionalBrowserOptions();
await this.provider.openBrowser(this.id, this.url, this.browserInfo.browserOption, additionalOptions);
if (this.status !== status_1.default.ready)
await (0, promisify_event_1.default)(this, 'ready');
this.status = status_1.default.opened;
this.emit('opened');
}
catch (err) {
this.emit('error', new runtime_1.GeneralError(types_1.RUNTIME_ERRORS.unableToOpenBrowser, this.browserInfo.providerName + ':' + this.browserInfo.browserName, err.stack));
}
}
async _closeBrowser(data = {}) {
if (!this.idle)
await (0, promisify_event_1.default)(this, 'idle');
try {
await this.provider.closeBrowser(this.id, data);
}
catch (err) {
// NOTE: A warning would be really nice here, but it can't be done while log is stored in a task.
this.debugLogger(err);
}
}
_forceIdle() {
if (!this.idle) {
this.idle = true;
this.emit('idle');
}
}
_createBrowserDisconnectedError() {
return new runtime_1.GeneralError(types_1.RUNTIME_ERRORS.browserDisconnected, this.userAgent);
}
_waitForHeartbeat() {
this.heartbeatTimeout = setTimeout(() => {
const err = this._createBrowserDisconnectedError();
this.status = status_1.default.disconnected;
this.testRunAborted = true;
this.emit('disconnected', err);
this._restartBrowserOnDisconnect(err);
}, this.HEARTBEAT_TIMEOUT);
}
async _getTestRunInfo(needPopNext) {
if (needPopNext || !this.pendingTestRunInfo)
this.pendingTestRunInfo = await this._popNextTestRunInfo();
return this.pendingTestRunInfo;
}
async _popNextTestRunInfo() {
while (this.hasQueuedJobs && !this.currentJob.hasQueuedTestRuns)
this.jobQueue.shift();
return this.hasQueuedJobs ? await this.currentJob.popNextTestRunInfo(this) : null;
}
getCurrentTestRun() {
return this._currentTestRun;
}
static getById(id) {
return tracker_1.default.activeBrowserConnections[id] || null;
}
async _restartBrowser() {
this.status = status_1.default.uninitialized;
this._forceIdle();
let resolveTimeout = null;
let isTimeoutExpired = false;
let timeout = null;
const restartPromise = (0, time_limit_promise_1.default)(this._closeBrowser({ isRestarting: true }), this.BROWSER_CLOSE_TIMEOUT, { rejectWith: new runtime_1.TimeoutError() })
.catch(err => this.debugLogger(err))
.then(() => this._runBrowser());
const timeoutPromise = new Promise(resolve => {
resolveTimeout = resolve;
timeout = setTimeout(() => {
isTimeoutExpired = true;
resolve();
}, this.BROWSER_RESTART_TIMEOUT);
});
return Promise.race([restartPromise, timeoutPromise])
.then(() => {
clearTimeout(timeout);
if (isTimeoutExpired)
this.emit('error', this._createBrowserDisconnectedError());
else
resolveTimeout();
});
}
_restartBrowserOnDisconnect(err) {
let resolveFn = null;
let rejectFn = null;
this.disconnectionPromise = new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = () => {
reject(err);
};
setTimeout(() => {
rejectFn();
});
})
.then(() => {
return this._restartBrowser();
})
.catch(e => {
this.emit('error', e);
});
this.disconnectionPromise.resolve = resolveFn;
this.disconnectionPromise.reject = rejectFn;
}
async getDefaultBrowserInitTimeout() {
const isLocalBrowser = await this.provider.isLocalBrowser(this.id, this.browserInfo.browserName);
return isLocalBrowser ? browser_connection_timeouts_1.LOCAL_BROWSER_INIT_TIMEOUT : browser_connection_timeouts_1.REMOTE_BROWSER_INIT_TIMEOUT;
}
async processDisconnection(disconnectionThresholdExceeded) {
const { resolve, reject } = this.disconnectionPromise;
if (disconnectionThresholdExceeded)
reject();
else
resolve();
}
addWarning(message, ...args) {
if (this.currentJob)
this.currentJob.warningLog.addWarning(message, ...args);
else
this.warningLog.addWarning(message, ...args);
}
_appendToPrettyUserAgent(str) {
this.browserInfo.parsedUserAgent.prettyUserAgent += ` (${str})`;
}
_moveWarningLogToJob(job) {
job.warningLog.copyFrom(this.warningLog);
this.warningLog.clear();
}
setProviderMetaInfo(str, options) {
const appendToUserAgent = options === null || options === void 0 ? void 0 : options.appendToUserAgent;
if (appendToUserAgent) {
// NOTE:
// change prettyUserAgent only when connection already was established
if (this.isReady())
this._appendToPrettyUserAgent(str);
else
this.on('ready', () => this._appendToPrettyUserAgent(str));
return;
}
this.browserInfo.userAgentProviderMetaInfo = str;
}
get userAgent() {
let userAgent = this.browserInfo.parsedUserAgent.prettyUserAgent;
if (this.browserInfo.userAgentProviderMetaInfo)
userAgent += ` (${this.browserInfo.userAgentProviderMetaInfo})`;
return userAgent;
}
get connectionInfo() {
if (!this.osInfo)
return this.userAgent;
const { name, version } = this.browserInfo.parsedUserAgent;
let connectionInfo = (0, parse_user_agent_1.calculatePrettyUserAgent)({ name, version }, this.osInfo);
const metaInfo = this.browserInfo.userAgentProviderMetaInfo || (0, parse_user_agent_1.extractMetaInfo)(this.browserInfo.parsedUserAgent.prettyUserAgent);
if (metaInfo)
connectionInfo += ` (${metaInfo})`;
return connectionInfo;
}
get retryTestPages() {
return this.browserConnectionGateway.retryTestPages;
}
get hasQueuedJobs() {
return !!this.jobQueue.length;
}
get currentJob() {
return this.jobQueue[0];
}
// API
runInitScript(code) {
return new Promise(resolve => this.initScriptsQueue.push({ code, resolve }));
}
addJob(job) {
this.jobQueue.push(job);
this._moveWarningLogToJob(job);
}
removeJob(job) {
(0, lodash_1.pull)(this.jobQueue, job);
}
async close() {
if (this.status === status_1.default.closing || this.status === status_1.default.closed)
return;
this.status = status_1.default.closing;
this.emit(status_1.default.closing);
await this._closeBrowser();
this.browserConnectionGateway.stopServingConnection(this);
if (this.heartbeatTimeout)
clearTimeout(this.heartbeatTimeout);
tracker_1.default.remove(this);
this.status = status_1.default.closed;
this.emit(status_1.default.closed);
}
async establish(userAgent) {
this.status = status_1.default.ready;
this.browserInfo.parsedUserAgent = (0, parse_user_agent_1.parseUserAgent)(userAgent);
this.osInfo = await this.provider.getOSInfo(this.id);
this._waitForHeartbeat();
this.emit('ready');
}
heartbeat() {
if (this.heartbeatTimeout)
clearTimeout(this.heartbeatTimeout);
this._waitForHeartbeat();
return {
code: this.status === status_1.default.closing ? heartbeat_status_1.default.closing : heartbeat_status_1.default.ok,
url: this.status === status_1.default.closing ? this.idleUrl : '',
};
}
renderIdlePage() {
return mustache_1.default.render(IDLE_PAGE_TEMPLATE, {
userAgent: this.connectionInfo,
statusUrl: this.statusUrl,
heartbeatUrl: this.heartbeatUrl,
initScriptUrl: this.initScriptUrl,
openFileProtocolUrl: this.openFileProtocolUrl,
retryTestPages: !!this.browserConnectionGateway.retryTestPages,
proxyless: this.proxyless,
});
}
getInitScript() {
const initScriptPromise = this.initScriptsQueue[0];
return { code: initScriptPromise ? initScriptPromise.code : null };
}
handleInitScriptResult(data) {
const initScriptPromise = this.initScriptsQueue.shift();
if (initScriptPromise)
initScriptPromise.resolve(JSON.parse(data));
}
isHeadlessBrowser() {
return this.provider.isHeadlessBrowser(this.id);
}
async reportJobResult(status, data) {
await this.provider.reportJobResult(this.id, status, data);
}
async getStatus(isTestDone) {
if (!this.idle && !isTestDone) {
this.idle = true;
this.emit('idle');
}
if (this.status === status_1.default.opened) {
const nextTestRunInfo = await this._getTestRunInfo(isTestDone || this.testRunAborted);
this.testRunAborted = false;
if (nextTestRunInfo) {
this.idle = false;
return {
cmd: command_1.default.run,
testRunId: nextTestRunInfo.testRunId,
url: nextTestRunInfo.url,
};
}
}
return {
cmd: command_1.default.idle,
url: this.idleUrl,
testRunId: null,
};
}
get activeWindowId() {
return this.provider.getActiveWindowId(this.id);
}
set activeWindowId(val) {
this.previousActiveWindowId = this.activeWindowId;
this.provider.setActiveWindowId(this.id, val);
}
async openFileProtocol(url) {
return this.provider.openFileProtocol(this.id, url);
}
async dispatchProxylessEvent(type, options) {
return this.provider.dispatchProxylessEvent(this.id, type, options);
}
async canUseDefaultWindowActions() {
return this.provider.canUseDefaultWindowActions(this.id);
}
isReady() {
return this.status === status_1.default.ready ||
this.status === status_1.default.opened ||
this.status === status_1.default.closing;
}
}
exports.default = BrowserConnection;
module.exports = exports.default;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYnJvd3Nlci9jb25uZWN0aW9uL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBQUEsa0RBQTBCO0FBQzFCLDRFQUEyQztBQUMzQyxtQ0FBc0M7QUFDdEMsd0RBQWdDO0FBQ2hDLG1DQUF3QztBQUN4QyxtRUFLc0M7QUFDdEMsMkRBQXNEO0FBQ3RELHNFQUE2QztBQUM3QyxtQ0FBZ0M7QUFDaEMsd0RBQWdDO0FBQ2hDLHNEQUErQztBQUMvQywwRUFBaUQ7QUFDakQsa0RBQWtFO0FBQ2xFLDhDQUFvRDtBQUdwRCxrRkFBeUQ7QUFHekQsc0VBQThDO0FBQzlDLHlGQU1pRDtBQUVqRCx3REFBaUQ7QUFRakQsTUFBTSw4QkFBOEIsR0FBRyxDQUFDLEVBQVUsRUFBVSxFQUFFLENBQUMsK0JBQStCLEVBQUUsRUFBRSxDQUFDO0FBRW5HLE1BQU0sa0JBQWtCLEdBQUcsSUFBQSw2QkFBSSxFQUFDLG9EQUFvRCxDQUFDLENBQUM7QUE2Q3RGLE1BQXFCLGlCQUFrQixTQUFRLHFCQUFZO0lBNkN2RCxZQUNJLE9BQWlDLEVBQ2pDLFdBQXdCLEVBQ3hCLFNBQWtCLEVBQ2xCLHNCQUFzQixHQUFHLEtBQUssRUFDOUIsU0FBUyxHQUFHLEtBQUssRUFDakIsVUFBdUI7UUFDdkIsS0FBSyxFQUFFLENBQUM7UUEzQ0osb0JBQWUsR0FBbUIsSUFBSSxDQUFDO1FBU3hDLFFBQUcsR0FBRyxFQUFFLENBQUM7UUFDVCxZQUFPLEdBQUcsRUFBRSxDQUFDO1FBQ1osa0JBQWEsR0FBRyxFQUFFLENBQUM7UUFDbkIsa0JBQWEsR0FBRyxFQUFFLENBQUM7UUFDcEIsaUJBQVksR0FBRyxFQUFFLENBQUM7UUFDbEIsY0FBUyxHQUFHLEVBQUUsQ0FBQztRQUNmLHNCQUFpQixHQUFHLEVBQUUsQ0FBQztRQUN2QixtQkFBYyxHQUFHLEVBQUUsQ0FBQztRQUNwQixrQkFBYSxHQUFHLEVBQUUs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