karma-selenium-grid-launcher
Version:
Launcher for Remote WebDriver instances on a Selenium Grid; forked from karma-webdriver-launcher.
446 lines (396 loc) • 15.1 kB
JavaScript
var wd = require('selenium-webdriver');
var urlModule = require('url');
var urlparse = urlModule.parse;
var urlformat = urlModule.format;
var chrome = require('selenium-webdriver/chrome');
var edge = require('selenium-webdriver/edge');
var firefox = require('selenium-webdriver/firefox');
var ie = require('selenium-webdriver/ie');
var safari = require('selenium-webdriver/safari');
var until = wd.until;
const CREATING_SESSION = 'CREATING_SESSION';
const CREATED_SESSION = 'CREATED_SESSION';
const WAITING = 'WAITING';
const ignoreArgs = ['base', 'gridUrl', 'suppressWarning', 'x-ua-compatible',
'heartbeatInterval', 'promptOn', 'delayLaunch', 'windowGeometry'];
// default preferences shamelessly taken from karma-firefox-launcher
const defaultFirefoxPrefs = {
'browser.shell.checkDefaultBrowser': false,
'browser.bookmarks.restore_default_bookmarks': false,
'dom.disable_open_during_load': false,
'dom.max_script_run_time': 0,
'extensions.autoDisableScopes': 0,
'browser.tabs.remote.autostart': false,
'browser.tabs.remote.autostart.2': false,
'extensions.enabledScopes': 15,
};
// default chrome args shamelessly taken from karma-chrome-launcher
const defaultChromeArgs = [
'--no-default-browser-check',
'--no-first-run',
'--disable-default-apps',
'--disable-popup-blocking',
'--disable-translate',
'--disable-background-timer-throttling',
// on macOS, disable-background-timer-throttling is not enough
// and we need disable-renderer-backgrounding too
// see https://github.com/karma-runner/karma-chrome-launcher/issues/123
'--disable-renderer-backgrounding',
'--disable-device-discovery-notifications',
];
const defaultIeOptions = {
browserAttachTimeout: 30000,
};
var SeleniumGridInstance = function (name, args, logger, baseLauncherDecorator,
captureTimeoutLauncherDecorator, retryLauncherDecorator) {
if (!args.browserName) {
throw new Error('browserName is required!');
}
var log = logger.create('SeleniumGrid');
var gridUrl = args.gridUrl || 'http://localhost:4444/wd/hub';
var self = this;
// Intialize capabilities with default values
const capabilities = new wd.Capabilities({
platform: 'ANY',
testName: 'Karma test',
version: ''
});
if (args.browserName === 'internet explorer') {
if (!args.suppressWarning) {
log.warn('Internet Explorer requires some specific configuration to work properly. ' +
'Follow the instructions on https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver#required-configuration ' +
'and then add `suppressWarning` to the launcher config.');
}
// workaround until IE options are properly implemented in selenium-webdriver
capabilities.set('se:ieOptions', defaultIeOptions);
}
const options = {};
options[wd.Browser.CHROME] = new chrome.Options();
options[wd.Browser.EDGE] = new edge.Options();
options[wd.Browser.FIREFOX] = new firefox.Options();
options[wd.Browser.IE] = new ie.Options();
options[wd.Browser.SAFARI] = new safari.Options();
Object.keys(defaultFirefoxPrefs).forEach((pref) => {
options[wd.Browser.FIREFOX].setPreference(pref, defaultFirefoxPrefs[pref]);
});
options[wd.Browser.CHROME].addArguments(defaultChromeArgs);
Object.keys(args).forEach(function (key) {
if (ignoreArgs.indexOf(key) !== -1) {
// used strictly for karma
return;
}
if (key === 'firefoxPreferences') {
Object.keys(args.firefoxPreferences).forEach((pref) => {
options[wd.Browser.FIREFOX].setPreference(pref,
args.firefoxPreferences[pref]);
});
return;
}
if ((key === 'arguments' || key === 'extensions' || key === 'options') &&
(!options[args.browserName])) {
throw new Error(key + ' not supported when using non-standard browser ' +
args.browserName + '; you must use the equivalent capability');
}
if (key === 'arguments') {
if (args[key].constructor !== Array) {
throw new Error('arguments must be an Array for ' + args.browserName);
}
if (!options[args.browserName].addArguments) {
throw new Error('arguments not supported for ' + args.browserName);
}
options[args.browserName].addArguments(args[key]);
return;
}
if (key === 'extensions') {
if (args[key].constructor !== Array) {
throw new Error('extensions must be an Array for ' + args.browserName);
}
if (!options[args.browserName].addExtensions) {
throw new Error('extensions not supported for ' + args.browserName);
}
options[args.browserName].addArguments(args[key]);
return;
}
if (key === 'options') {
if (!typeof args[key] === 'object') {
throw new Error('options must be an object for ' + args.browserName);
}
Object.keys(args[key]).forEach((option) => {
if (!options[args.browserName][option]) {
throw new Error('option ' + option + ' not supported for ' + args.browserName);
}
options[args.browserName][option](args[key][option]);
});
return;
}
capabilities.set(key, args[key]);
});
baseLauncherDecorator(this);
captureTimeoutLauncherDecorator(this);
retryLauncherDecorator(this);
self.name = name;
self.browser = null;
var heartbeatErrors = 0;
var heartbeat;
const heartbeatFunction = () => {
log.debug('hearbeat for ' + self.name);
self.browser.getTitle()
.catch((err) => {
heartbeatErrors++;
log.error('Caught error for browser ' + self.name + ' during ' +
'heartbeat: ' + err);
if (err.name !== 'NoSuchSessionError') {
log.error(err.stack);
}
if (heartbeatErrors >= 5) {
log.error('Too many heartbeat errors, attempting to stop ' + self.name);
args.heartbeatInterval && clearInterval(heartbeat);
this.error = this.error || err;
this.kill();
}
});
};
const promptFunction = (elId) => {
return new Promise((resolve, reject) => {
log.info('inside prompt function for ' + self.name);
// sometimes we get stuck inside the prompt function when quitting,
// so reject after a fixed amount of time just to unblock things
let bumpTimeout = setTimeout(() => reject('prompt function stuck'), 10000);
var windowRef;
self.browser.getWindowHandle().then((ref) => {
windowRef = ref;
return self.browser.switchTo().frame(0);
})
.then(() => self.browser.wait(until.elementLocated(wd.By.id(elId))))
.then(() => {
log.debug('Trying to focus ' + self.name + ' with an alert...');
return self.browser.switchTo().frame(null);
})
.then(() => self.browser.switchTo().window(windowRef))
.then(() => self.browser.executeScript("alert('test')"))
.then(() => self.browser.switchTo().alert())
.then((alert) => alert.dismiss())
.then(() => self.browser.switchTo().window(windowRef))
.then(() => {
resolve();
clearTimeout(bumpTimeout);
})
.catch((err) => {
log.error('caught in prompt for ' + self.name + ': ' + err);
clearTimeout(bumpTimeout);
reject(err);
});
});
};
// This is done by passing the option on the url, in response the Karma server will
// set the following meta in the page.
// <meta http-equiv="X-UA-Compatible" content="[VALUE]"/>
function handleXUaCompatible(urlObj) {
if (args['x-ua-compatible']) {
urlObj.query['x-ua-compatible'] = args['x-ua-compatible'];
}
}
var loadTimeout = null;
var started;
var startPromise;
this.awaitingKill = () => {
return this.state === this.STATE_BEING_KILLED ||
this.state === this.STATE_BEING_FORCE_KILLED;
};
this.on('start', (url) => {
// reset kill state and error state so we don't auto-end the session immediately
this.error = null;
var urlObj = urlparse(url, true);
handleXUaCompatible(urlObj);
delete urlObj.search; //url.format does not want search attribute
url = urlformat(urlObj);
log.debug('Grid URL: ' + gridUrl);
log.debug('Browser capabilities: ' + JSON.stringify(capabilities));
let delayTime = 0;
if (args.delayLaunch) {
log.debug('Delaying launch of ' + args.browserName + ' for ' + args.delayLaunch + 'ms');
this.state = WAITING;
delayTime = args.delayLaunch;
}
loadTimeout = setTimeout(() => {
let errorStage;
log.debug('Launching ' + self.name);
this.state = CREATING_SESSION;
startPromise = new Promise((resolve, reject) => {
self.browser = new wd.Builder()
.setChromeOptions(options[wd.Browser.CHROME])
.setEdgeOptions(options[wd.Browser.EDGE])
.setFirefoxOptions(options[wd.Browser.FIREFOX])
.setIeOptions(options[wd.Browser.IE])
.setSafariOptions(options[wd.Browser.SAFARI])
.usingServer(gridUrl)
.withCapabilities(capabilities)
.build()
self.browser.then(() => {
log.debug(self.name + ' started');
resolve();
started = true;
this.state = CREATED_SESSION;
startPromise = startPromise.then(() => {
log.debug(self.name + ' resolved startPromise; navigating to karma page');
});
startPromise = startPromise.then(() => self.browser.get(url))
.then(() => {
log.debug(self.name + ' loaded karma; running applicable initialization');
return Promise.resolve();
})
.catch(err => {
errorStage = 'navigate';
return Promise.reject(err);
});
if (args.windowGeometry) {
startPromise = startPromise.then(() => {
log.debug(self.name + ' setting window geometry');
return self.browser.manage().window().setRect(args.windowGeometry).then(() => {
log.debug(self.name + ' set window geometry');
return Promise.resolve();
});
});
}
if (args.promptOn) {
startPromise = startPromise.then(() => promptFunction(args.promptOn));
}
if (args.heartbeatInterval) {
// TODO: This should maybe be before the `get` call?
startPromise = startPromise.then(() => {
log.debug(self.name + ' setting heartbeat');
heartbeat = setInterval(heartbeatFunction, args.heartbeatInterval);
return Promise.resolve();
});
}
startPromise = startPromise.then(() => {
log.debug(self.name + ' initialized');
return Promise.resolve();
});
startPromise = startPromise.catch((err) => {
if (errorStage) {
// re-raise error caught during navigation
return Promise.reject(err);
} else {
errorStage = 'init';
return Promise.reject(err);
}
});
return startPromise;
})
.catch((err) => {
if (this.awaitingKill()) {
log.debug('ignoring error from ' + self.name + '; browser is shutting down');
reject(err);
return Promise.resolve();
} else {
started = false;
let message;
if (errorStage === 'init') {
message = self.name + ' encountered error during initialization: ' + err;
} else if (errorStage === 'navigate') {
message = self.name + ' encountered error navigating to karma page: ' + err;
} else {
message = self.name + ' was unable to create WebDriver session: ' + err;
}
log.error(message);
this.error = err;
reject(message);
self._done();
}
}); // self.browser.then
}); // startPromise
}, delayTime);
});
let killInterval;
let killElapsed = 0;
this._stopSession = (done, startError) => {
var self = this;
let killPromise = Promise.resolve();
clearInterval(heartbeat);
if (!started) {
log.info(self.name + ' not started... Killed ' + self.name);
done();
return killPromise;
}
if (args.resetBeforeQuit) {
// load a blank page and wait before quitting. for browsers that don't
// close the window before quitting. noticed on real iOS 12 devices.
killPromise = killPromise.then(() => {
log.debug('Resetting ' + self.name + ' and pausing.');
return new Promise((resolve, reject) => {
self.browser.get('about:blank').then(() => setTimeout(() => {
log.debug('Reset ' + self.name + ' to about:blank');
resolve();
}, 10000));
});
});
}
if (args.closeBeforeQuit) {
// explicitly call browser.close(), which should be unnecessary according
// to webdriver. noticed on real iOS 10 devices.
killPromise = killPromise.then(() => {
log.debug('Closing browser window for ' + self.name + '.');
return self.browser.close().then(() => {
log.debug('Closed browser window for ' + self.name);
this.state = 'CLOSED';
return Promise.resolve();
});
});
}
killPromise = killPromise.then(() => {
log.debug('Quitting WebDriver for ' + self.name + '.');
return self.browser.quit()
.then(() => {
log.info('Killed ' + self.name + '.');
done();
return Promise.resolve();
});
});
killPromise = killPromise.catch((err) => {
if (err.name !== 'NoSuchSessionError' && err !== startError) {
// ignore NoSuchSessionErrors when killing
log.error('Error stopping browser ' + self.name + ': ' + err.toString());
}
done();
return Promise.resolve();
});
return killPromise;
};
this.on('kill', (done) => {
var self = this;
log.info('Trying to kill ' + self.name);
const end = () => {
self._done();
if (done) {
done();
}
};
const stopSession = (err) => {
return new Promise((startPromiseResolve, startPromiseReject) => {
this._stopSession(end, err).then(() => {
clearInterval(killInterval);
resolve('shutting down');
startPromiseReject('shutting down');
});
});
};
if (!self.browser) {
log.info('Browser ' + self.name + ' has not yet launched.');
loadTimeout && clearTimeout(loadTimeout);
end();
return Promise.resolve();
}
killInterval = setInterval(() => {
killElapsed += 10;
log.info('Waiting for ' + self.name + ' to quit... (' + killElapsed + 's)');
}, 10000);
return new Promise((resolve, reject) => {
startPromise = startPromise.then(stopSession, stopSession);
});
});
};
// PUBLISH DI MODULE
module.exports = {
'launcher:SeleniumGrid': ['type', SeleniumGridInstance]
};