gemini
Version:
UI Screenshot testing utility
331 lines (270 loc) • 10.2 kB
JavaScript
'use strict';
const debug = require('debug');
const chalk = require('chalk');
const _ = require('lodash');
const Promise = require('bluebird');
const striptags = require('striptags');
const Browser = require('./browser');
const ClientBridge = require('./client-bridge');
const GeminiError = require('../errors/gemini-error');
const WdErrors = require('../constants/wd-errors');
const OPERA_NOT_SUPPORTED = 'Not supported in OperaDriver yet';
module.exports = class NewBrowser extends Browser {
constructor(config) {
super(config);
this.log = debug(`gemini:browser: ${this.id}`);
const wdLog = debug(`gemini:webdriver: ${this.id}`);
this._wd.on('connection', (code, message) => wdLog(`Error: code ${code}, ${message}`));
this._wd.on('status', (info) => wdLog(info));
this._wd.on('command', (eventType, command, response) => {
if (eventType === 'RESPONSE' && command === 'takeScreenshot()') {
response = '<binary-data>';
}
if (typeof response !== 'string') {
response = JSON.stringify(response);
}
wdLog(chalk.cyan(eventType), command, chalk.grey(response || ''));
});
this._exposeWdApi([
'sleep',
'waitForElementByCssSelector',
'waitFor',
'moveTo',
'click',
'doubleclick',
'buttonDown',
'buttonUp',
'keys',
'type',
'tapElement',
'flick',
'execute',
'setWindowSize',
'getWindowSize',
// 'setOrientation' and 'getOrientation' work only in context 'NATIVE_APP' with 'appium' below 1.5.x
{name: 'getOrientation', context: 'NATIVE_APP'},
{name: 'setOrientation', context: 'NATIVE_APP'}
]);
}
_exposeWdApi(methods) {
methods
.map((method) => _.isPlainObject(method) ? method : {name: method})
.forEach((method) => this[method.name] = this._exposeWdMethod(method));
}
_exposeWdMethod(method) {
return function() {
return method.context
? this._applyWdMethodInContext(method.name, method.context, arguments)
: this._applyWdMethod(method.name, arguments);
};
}
_applyWdMethodInContext(method, context, args) {
return this._wd.currentContext()
.then((originalContext) => {
return this._wd.context(context)
.then(() => this._applyWdMethod(method, args))
.finally(() => this._wd.context(originalContext));
});
}
_applyWdMethod(method, args) {
return this._wd[method].apply(this._wd, args);
}
launch(calibrator) {
return this.initSession()
.then(() => this._setDefaultSize())
.then(() => this._setDefaultOrientation())
.then(() => {
// maximize is required, because default
// windows size in phantomjs can prevent
// some shadows from fitting in
if (this._shouldMaximize()) {
return this._maximize();
}
})
.then(() => {
if (!this.config.calibrate || this._calibration) {
return;
}
return calibrator.calibrate(this)
.then((calibration) => this._setCalibration(calibration));
})
.then(() => this.buildScripts())
.then(() => this.chooseLocator())
.catch((e) => {
if (e.code === 'ECONNREFUSED') {
return Promise.reject(new GeminiError(
`Unable to connect to ${this.config.gridUrl}.`,
'Make sure that URL in config file is correct and selenium\nserver is running.'
));
}
const error = new GeminiError(`Cannot launch browser ${this.id}:\n${e.message}.`);
if (e.data) {
error.message += '\nReason: ' + this._parseHtmlBody(e.data);
}
error.browserId = this.id;
error.sessionId = this.sessionId;
// selenium does not provide a way to distinguish different reasons of failure
return Promise.reject(error);
});
}
_parseHtmlBody(html) {
const body = html.match(/<body[\s\S]*<\/body>/i);
if (!body) {
return html;
}
const rawText = striptags(body[0]);
return rawText
? (rawText).replace(/\s+/g, ' ').trim()
: html;
}
_setSessionTimeout(timeout) {
if (_.isNull(timeout)) {
timeout = this.config.httpTimeout;
}
return this._configureHttp(timeout);
}
initSession() {
return this._setSessionTimeout(this.config.sessionRequestTimeout)
.then(() => this._wd.init(this.capabilities))
.spread((sessionId) => {
this.sessionId = sessionId;
this.log('launched session %o', this);
})
.then(() => this._setHttpTimeout());
}
_setDefaultSize() {
const size = this.config.windowSize;
if (!size) {
return;
}
return this._wd.setWindowSize(size.width, size.height)
.catch((e) => {
// Its the only reliable way to detect not supported operation
// in legacy operadriver.
const message = _.get(e, 'cause.value.message');
if (message === OPERA_NOT_SUPPORTED) {
console.warn(chalk.yellow('WARNING!'));
console.warn('Legacy Opera Driver does not support window resizing');
console.warn('windowSize setting will be ignored.');
return;
}
return Promise.reject(e);
});
}
_setDefaultOrientation() {
const orientation = this.config.orientation;
if (!orientation) {
return;
}
return this._wd.setOrientation(orientation);
}
openRelative(relativeURL) {
return this.open(this.config.getAbsoluteUrl(relativeURL), {resetZoom: true});
}
// Zoom reset should be skipped before calibration cause we're unable to build client scripts before
// calibration done. Reset will be executed as 1 of calibration steps.
open(url, params) {
params = _.defaults(params || {}, {
resetZoom: false
});
return this._wd.get(url)
.then((url) => {
return params.resetZoom
? this._clientBridge.call('resetZoom').then(() => url)
: url;
});
}
injectScript(script) {
return this._wd.execute(script);
}
evalScript(script) {
return this._wd.eval(script);
}
buildScripts() {
return ClientBridge.build(this, {calibration: this._calibration, coverage: this._coverageEnabled, supportDeprecated: true})
.then((clientBridge) => this._clientBridge = clientBridge);
}
get _coverageEnabled() {
return this.config.system.coverage.enabled;
}
get _needsCompatLib() {
return this._calibration && this._calibration.needsCompatLib;
}
chooseLocator() {
this.findElement = this._needsCompatLib ? this._findElementScript : this._findElementWd;
}
reset() {
// We can't use findElement here because it requires page with body tag
return this.evalScript('document.body')
.then(body => this._wd.moveTo(body, 0, 0))
.catch(e => {
return Promise.reject(_.extend(e || {}, {
browserId: this.id,
sessionId: this.sessionId
}));
});
}
get browserName() {
return this.capabilities.browserName;
}
get version() {
return this.capabilities.version;
}
get capabilities() {
return this.config.desiredCapabilities;
}
_shouldMaximize() {
if (this.config.windowSize) {
return false;
}
return this.browserName === 'phantomjs';
}
_maximize() {
return this._wd.windowHandle()
.then((handle) => this._wd.maximize(handle));
}
findElement() {
throw new Error('findElement is called before appropriate locator is chosen');
}
_findElementWd(selector) {
return this._wd.elementByCssSelector(selector)
.catch((error) => {
if (error.status === WdErrors.ELEMENT_NOT_FOUND) {
error.selector = selector;
}
return Promise.reject(error);
});
}
_findElementScript(selector) {
return this._clientBridge.call('queryFirst', [selector])
.then((element) => {
if (element) {
return element;
}
const error = new Error('Unable to find element');
error.status = WdErrors.ELEMENT_NOT_FOUND;
error.selector = selector;
return Promise.reject(error);
});
}
prepareScreenshot(selectors, opts) {
opts = _.extend(opts, {
usePixelRatio: this._calibration ? this._calibration.usePixelRatio : true,
coverage: this._coverageEnabled
});
return this._clientBridge.call('prepareScreenshot', [selectors, opts]);
}
quit() {
if (!this.sessionId) {
return Promise.resolve();
}
return this._setSessionTimeout(this.config.sessionQuitTimeout)
.then(() => this._wd.quit())
.then(() => this.log('kill browser %o', this))
.then(() => this._setHttpTimeout())
.catch((err) => this.log(err));
}
inspect() {
return `[${this.id} (${this.sessionId})]`;
}
};