Skip to content
This repository has been archived by the owner on Sep 21, 2022. It is now read-only.

Commit

Permalink
Allow to use Sizzle for browsers without CSS3
Browse files Browse the repository at this point in the history
Browsers without CSS3 selectors support will use Sizzle selectors
library. Ability to use CSS3 selectors is determined during
calibration. If calibration is disabled, it is assumed that browser
has CSS3 selectors.
findElement method of browser will be also implemented with Sizzle
if CSS3-selectors are not available, so it will be possible to use
such selector as both actions and capture/ignore targets.

ClientBridge class added which encapsulates all inject/eval client
script logic previously found in browser.
  • Loading branch information
Sergey Tatarintsev committed May 18, 2015
1 parent 58d6ee2 commit a57069f
Show file tree
Hide file tree
Showing 14 changed files with 406 additions and 115 deletions.
82 changes: 82 additions & 0 deletions lib/browser/client-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict';

var util = require('util'),
q = require('q'),

StateError = require('../errors/state-error'),

NO_CLIENT_FUNC = 'ERRNOFUNC';

/**
* @param {Browser} browser
* @param {String} script
* @constructor
*/
function ClientBridge(browser, script) {
this._browser = browser;
this._script = script;
}

/**
* @param {String} name
* @param {Array} [args]
*/
ClientBridge.prototype.call = function(name, args) {
args = args || [];
return this._callCommand(this._clientMethodCommand(name, args), true);
};

/**
* @param {String} command
* @param {Boolean} shouldInject
*/
ClientBridge.prototype._callCommand = function(command, injectAllowed) {
var _this = this;
return this._browser.evalScript(command)
.then(function(result) {
if (!result || !result.error) {
return q.resolve(result);
}

if (result.error !== NO_CLIENT_FUNC) {
return q.reject(new StateError(result.message));
}

if (injectAllowed) {
return _this._inject()
.then(function() {
return _this._callCommand(command, false);
});
}
return q.reject(new StateError('Unable to inject gemini client script'));
});
};

/**
* @param {String} name
* @param {Array} args
*/
ClientBridge.prototype._clientMethodCommand = function(name, args) {
var call = util.format('__gemini.%s(%s)',
name,
args.map(JSON.stringify).join(', ')
);
return this._guardClientCall(call);
};

/**
* @param {String} call
*/
ClientBridge.prototype._guardClientCall = function(call) {
return util.format(
'typeof __gemini !== "undefined"? %s : {error: "%s"})',
call,
NO_CLIENT_FUNC
);
};

ClientBridge.prototype._inject = function() {
return this._browser.evalScript(this._script);
};

module.exports = ClientBridge;
38 changes: 28 additions & 10 deletions lib/browser/client-scripts/gemini.calibrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,34 @@
document.body.appendChild(div);
}

var bodyStyle = document.body.style;
bodyStyle.margin = 0;
bodyStyle.padding = 0;
function createPattern() {
var bodyStyle = document.body.style;
bodyStyle.margin = 0;
bodyStyle.padding = 0;

if (needsResetBorder()) {
bodyStyle.border = 0;
if (needsResetBorder()) {
bodyStyle.border = 0;
}
bodyStyle.backgroundColor = '#00ff00';
bodyStyle.width = '100%';
bodyStyle.height = '100%';
createRedStripe('left');
createRedStripe('right');
}
bodyStyle.backgroundColor = '#00ff00';
bodyStyle.width = '100%';
bodyStyle.height = '100%';
createRedStripe('left');
createRedStripe('right');

function getBrowserFeatures() {
var features = {
hasCSS3Selectors: true
};
try {
document.querySelector('body:nth-child(1)');
} catch (e) {
features.hasCSS3Selectors = false;
}

return features;
}

createPattern();
return getBrowserFeatures();
}(window));
7 changes: 4 additions & 3 deletions lib/browser/client-scripts/gemini.coverage.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict';

var util = require('./util'),
rect = require('./rect');
rect = require('./rect'),
query = require('./query');

exports.collectCoverage = function collectCoverage(rect) {
var coverage = {},
Expand Down Expand Up @@ -66,14 +67,14 @@ function coverageForRule(rule, area, ctx) {

util.each(rule.selectorText.split(','), function(selector) {
var within,
matches = document.querySelectorAll(selector);
matches = query.all(selector);

selector = selector.trim();

var re = /:{1,2}(?:after|before|first-letter|first-line|selection)(:{1,2}\w+)?$/;
// if selector contains pseudo-elements cut it off and try to find element without it
if (matches.length === 0 && re.test(selector)) {
matches = document.querySelectorAll(selector.replace(re, '$1'));
matches = query.all(selector.replace(re, '$1'));
}

if (matches.length > 0) {
Expand Down
7 changes: 5 additions & 2 deletions lib/browser/client-scripts/gemini.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

var util = require('./util'),
rect = require('./rect'),
query = require('./query'),
Rect = rect.Rect;

global.__gemini = exports;

exports.query = query;

// Terminology
// - clientRect - the result of calling getBoundingClientRect of the element
// - extRect - clientRect + outline + box shadow
Expand Down Expand Up @@ -69,7 +72,7 @@ function prepareScreenshotUnsafe(selectors, opts) {
function getCaptureRect(selectors) {
var element, elementRect, rect;
for (var i = 0; i < selectors.length; i++) {
element = document.querySelector(selectors[i]);
element = query.first(selectors[i]);
if (!element) {
return {
error: 'NOTFOUND',
Expand All @@ -89,7 +92,7 @@ function getCaptureRect(selectors) {
function findIgnoreAreas(selectors) {
var result = [];
util.each(selectors, function(selector) {
var element = document.querySelector(selector);
var element = query.first(selector);
if (element) {
result.push(getElementCaptureRect(element).round().serialize());
}
Expand Down
9 changes: 9 additions & 0 deletions lib/browser/client-scripts/query.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

exports.first = function(selector) {
return document.querySelector(selector);
};

exports.all = function(selector) {
return document.querySelectorAll(selector);
};
12 changes: 12 additions & 0 deletions lib/browser/client-scripts/query.sizzle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';
/*jshint newcap:false*/
var Sizzle = require('sizzle');

exports.first = function(selector) {
var elems = Sizzle(selector + ':first');
return elems.length > 0? elems[0] : null;
};

exports.all = function(selector) {
return Sizzle(selector);
};
92 changes: 52 additions & 40 deletions lib/browser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ var path = require('path'),
Image = require('../image'),
Actions = require('./actions'),

GeminiError = require('../errors/gemini-error'),
StateError = require('../errors/state-error');
ClientBridge = require('./client-bridge'),

module.exports = inherit({
GeminiError = require('../errors/gemini-error');

var Browser = inherit({
__constructor: function(config, id) {
this.config = config;
this._capabilities = config.browsers[id];
Expand Down Expand Up @@ -81,6 +82,9 @@ module.exports = inherit({
.then(function() {
return _this.buildScripts();
})
.then(function() {
return _this.chooseLocator();
})
.fail(function(e) {
if (e.code === 'ECONNREFUSED') {
return q.reject(new GeminiError(
Expand Down Expand Up @@ -132,12 +136,9 @@ module.exports = inherit({
return this._browser.get(url);
},

injectScripts: function() {
return this.inject(this._scripts);
},

inject: function(script) {
return this._browser.execute(script);
evalScript: function(script) {
/*jshint evil:true*/
return this._browser.eval(script);
},

buildScripts: function() {
Expand All @@ -151,16 +152,32 @@ module.exports = inherit({
}

script.transform({sourcemap: false, global: true}, 'uglifyify');
var queryLib = this._needsSizzle? './query.sizzle.js' : './query.native.js';
script.transform({
aliases: {
'./query': {relative: queryLib}
},
verbose: false
}, 'aliasify');

var _this = this;

return q.nfcall(script.bundle.bind(script))
.then(function(buf) {
_this._scripts = _this._polyfill + '\n' + buf.toString();
return _this._scripts;
var scripts = _this._polyfill + '\n' + buf.toString();
_this._clientBridge = new ClientBridge(_this, scripts);
return scripts;
});
},

get _needsSizzle() {
return this._calibration && !this._calibration.hasCSS3Selectors;
},

chooseLocator: function() {
this.findElement = this._needsSizzle? this._findElementScript : this._findElementWd;
},

reset: function() {
var _this = this;
return this.findElement('body')
Expand Down Expand Up @@ -193,49 +210,40 @@ module.exports = inherit({
});
},

_findElements: function(selectorsList) {
var _this = this;
return q.all(selectorsList.map(function(selector) {
return _this.findElement(selector, true);
}));
findElement: function(selector) {
throw new Error('findElement is called before appropriate locator is chosen');
},

findElement: function(selector) {
_findElementWd: function(selector) {
return this._browser.elementByCssSelector(selector)
.then(function(wdElement) {
return wdElement;
})
.fail(function(error) {
if (error.status === 7) {
if (error.status === Browser.ELEMENT_NOT_FOUND) {
error.selector = selector;
}
return q.reject(error);
});
},

prepareScreenshot: function(selectors, opts) {
/*jshint evil:true*/
var _this = this;
opts = opts || {};
return this._browser.eval(this._prepareScreenshotCommand(selectors, opts))
.then(function(data) {
if (data.error) {
if (data.error !== 'ERRNOFUNC') {
return q.reject(new StateError(data.message));
}

return _this.injectScripts()
.then(function() {
return _this.prepareScreenshot(selectors, opts);
});
_findElementScript: function(selector) {
return this._clientBridge.call('query.first', [selector])
.then(function(element) {
if (element) {
return element;
}
return q.resolve(data);

var error = new Error('Unable to find element');
error.status = Browser.ELEMENT_NOT_FOUND;
error.selector = selector;
return q.reject(error);
});
},

_prepareScreenshotCommand: function(selectors, opts) {
return 'typeof(__gemini) !== "undefined"? __gemini.prepareScreenshot(' + JSON.stringify(selectors) + ', ' +
JSON.stringify(opts) + ') : {error: "ERRNOFUNC"}';
prepareScreenshot: function(selectors, opts) {
opts = opts || {};
return this._clientBridge.call('prepareScreenshot', [
selectors,
opts
]);
},

captureFullscreenImage: function() {
Expand Down Expand Up @@ -264,4 +272,8 @@ module.exports = inherit({
return new Actions(this);
}

}, {
ELEMENT_NOT_FOUND: 7
});

module.exports = Browser;
Loading

0 comments on commit a57069f

Please sign in to comment.