jwebdriver
Version:
A webdriver client for node.js
947 lines (898 loc) • 25.8 kB
JavaScript
/**
* This class is used for control elements.
* @class Elements
*/
const PromiseClass = require('promiseclass');
const Elements = PromiseClass.create({
/**
* init elements instance
* @method constructor
* @private
* @param {Browser} browser Browser instance
* @param {string} [using] find mode: class name|css selector|id|name|link text|partial link text|tag name|xpath
* @param {string} value find pattern
*/
constructor(browser, using, value){
let self = this;
self._browser = browser;
self.using = using;
self.value = value;
self.MouseButtons = browser.MouseButtons;
self.Keys = browser.Keys;
},
/**
* init elements
* @method init
* @private
* @param {Function} done callback function
*/
init(done){
let self = this;
let browser = self._browser;
let using = self.using;
let value = self.value;
if(value === undefined){
value = using;
using = undefined;
}
if(using === undefined){
// get current active element
if(value === 'active'){
return browser.execCmd('getActiveElement', function(error, ret){
if(error){
done(error);
}
else{
self.elementIds = [ret.value.ELEMENT];
self.length = 1;
done();
}
});
}
else{
using = /^\.?\//.test(value)?'xpath':'css selector';
self.using = using;
self.value = value;
}
}
if(using === 'elements'){
self.elementIds = value;
self.length = value.length;
return done();
}
else{
browser.execCmd('findElements', {}, {
using: using,
value: value
}, function(error, ret){
if(error){
done(error);
}
else{
let arrElements = ret.value;
if(arrElements.length > 0){
let elementIds = arrElements.map(function(element){
return element.ELEMENT;
});
self.elementIds = elementIds;
self.length = elementIds.length;
done();
}
else{
self.elementIds = [];
self.length = 0;
let message = 'Find elements failed: ' + using + ', ' + value;
self.addLog('ERROR', message);
done(message);
}
}
});
}
},
/**
* add log
* @method addLog
* @public
* @param {COMMAND|DATA|RESULT|ERROR|WARNING|INFO} type log type
* @param {Object} message log message
*/
addLog(type, message){
this._browser.addLog(type, message);
},
/**
* execute protocal command with this elements
* @method execCmd
* @public
* @param {String} cmd protocal command, defined in command.js
* @param {Object} [pathData] replace the path parameters, no need to add sessionId
* @param {Object} [data] send data to protocal api
* @param {Function} done callback function
* @return {Object} the return object from webdriver server
*/
execCmd(cmd, pathData, data, done){
let self = this;
if(typeof pathData === 'function'){
pathData = undefined;
data = undefined;
}
else if(typeof data === 'function'){
data = undefined;
}
done = getDone(arguments);
if(self.length > 0){
let browser = self._browser;
let elementIds = self.elementIds.concat();
pathData = pathData || {};
if(/^!/.test(cmd)){
cmd = cmd.substr(1);
function execNext(){
let id = elementIds.shift();
if(id !== undefined){
pathData.id = id;
browser.execCmd(cmd, pathData, data, function(error){
if(error){
done(error);
}
else{
execNext();
}
});
}
else{
done();
}
}
execNext();
}
else{
pathData.id = elementIds[0];
browser.execCmd(cmd, pathData, data, function(error, ret){
done(error, ret);
});
}
}
else{
done('Elements empty: '+ self.value+' ('+self.using+')');
}
},
/**
* sleep sync
* @method sleep
* @public
* @param {Number} ms millisecond
* @param {Function} done callback function
*/
sleep(ms, done){
this._browser.sleep(ms, done);
},
/**
* get webdriver ELEMENT object
* @method toJSON
* @param {Boolean} first
* @public
* @return {Object|Array} [{ELEMENT: 1}] | {ELEMENT: 1}
*/
toJSON(first){
var elementIds = this.elementIds;
if(first && elementIds.length > 0){
return {ELEMENT: elementIds[0]};
}
else{
var arrJson = [];
elementIds.forEach(function(id){
arrJson.push({ELEMENT: id});
});
return arrJson;
}
},
/**
* get new element from start to end
* @method slice
* @public
* @param {Number} start
* @param {Number} end
* @param {Boolean} [changeSelf]
*/
slice(start, end, changeSelf, done){
let self = this;
if(typeof changeSelf === 'function'){
changeSelf = false;
}
done = getDone(arguments);
if(start >= 0 && start < self.length && end > start && end <= self.length){
let newElementIds = self.elementIds.slice(start, end);
let newElements = new Elements(self._browser, 'elements', newElementIds);
newElements.init();
if(changeSelf){
self.elementIds = newElementIds;
self.length = newElementIds.length;
}
done(null, newElements);
}
else{
done('Elements slice range error.');
}
},
/**
* get new element by index
* @method get
* @public
* @param {Number} index
* @param {Boolean} [changeSelf]
*/
get(index, changeSelf, done){
done = getDone(arguments);
this.slice(index, index+1, changeSelf, done);
},
/**
* get new first element
* @method first
* @public
* @param {Boolean} [changeSelf]
*/
first(changeSelf, done){
done = getDone(arguments);
this.get(0, changeSelf, done);
},
/**
* get new last element
* @method last
* @public
* @param {Boolean} [changeSelf]
*/
last(changeSelf, done){
var self = this;
done = getDone(arguments);
self.get(self.length-1, changeSelf, done);
},
/**
* get tagName (first element)
* @method tagName
* @public
* @param {Function} done callback function
* @return {String}
*/
tagName(done){
this.execCmd('getElementTagName', function(error, ret){
done(error, ret && ret.value);
});
},
/**
* get attribute value (first element)
* @method attr
* @public
* @param {String} name attribute name
* @param {Function} done callback function
* @return {String}
*/
attr(name, done){
this.execCmd('getElementAttribute', {
name: name
}, function(error, ret){
done(error, ret && ret.value);
});
},
/**
* get property value (first element)
* @method prop
* @public
* @param {String} name attribute name
* @param {Function} done callback function
* @return {Object} return proerty value
*/
prop(name, done){
this.execCmd('getElementProperty', {name: name}, function(error, ret){
done(error, ret && ret.value);
});
},
/**
* get rect(first element)
* @method prop
* @public
* @param {Function} done callback function
* @return {Object} return rect info
*/
rect(done){
this.execCmd('getElementRect', function(error, ret){
done(error, ret && ret.value);
});
},
/**
* get css value (first element)
* @method css
* @public
* @param {String} name css name
* @param {Function} done callback function
* @return {String}
*/
css(name, done){
this.execCmd('getElementCss', {
propertyName: name
}, function(error, ret){
done(error, ret && ret.value);
});
},
/**
* get text (first element)
* @method text
* @public
* @param {Function} done callback function
* @return {String}
*/
text(done){
this.execCmd('getElementText', function(error, ret){
done(error, ret && ret.value);
});
},
/**
* clear input or textarea
* @method clear
* @public
* @param {Function} done callback function
* @return {this}
*/
clear(done){
this.execCmd('!setElementClear', done);
},
/**
* get offset of element (first element)
* @method offset
* @public
* @param {Boolean} [isInview]
* @param {Function} done callback function
* @return {Object} return {x:1,y:1}
*/
offset(isInview, done){
let self = this;
if(typeof isInview === 'function'){
isInview = undefined;
}
done = getDone(arguments);
if(isInview){
self.execCmd('getElementOffsetInView', function(error, ret){
done(error, ret && ret.value);
});
}
else{
self.execCmd('getElementOffset', function(error, ret){
done(error, ret && ret.value);
});
}
},
/**
* get size of element (first element)
* @method size
* @public
* @param {Function} done callback function
* @return {Object} return {width:1, height:1}
*/
size(done){
this.execCmd('getElementSize', function(error, ret){
done(error, ret && ret.value);
});
},
/**
* check element displayed (first element)
* @method displayed
* @public
* @param {Function} done callback function
* @return {Boolean}
*/
displayed(done){
this.execCmd('getElementDisplayed', function(error, ret){
done(null, ret && ret.value);
});
},
/**
* check element enabled (first element)
* @method enabled
* @public
* @param {Function} done callback function
* @return {Boolean}
*/
enabled(done){
this.execCmd('getElementEnabled', function(error, ret){
done(null, ret && ret.value);
});
},
/**
* check element selected (first element)
* @method selected
* @public
* @param {Function} done callback function
* @return {Boolean}
*/
selected(done){
this.execCmd('getElementSelected', function(error, ret){
done(null, ret && ret.value);
});
},
/**
* select option
* @method select
* @public
* @param {Number|String|Object} value {type:'index', value:'test'} type:index | value | text
* @param {Function} done callback function
* @return {Boolean}
*/
select(value, done){
let type = 'index';
if(typeof value === 'number'){
type = 'index';
}
else if(typeof value === 'string'){
type = 'value';
}
else{
type = value.type;
value = value.value;
}
let filter;
let quote = /"/.test(value) ? "'" : '"';
switch(type){
case 'index':
filter = '';
value = value && parseInt(value, 10);
break;
case 'value':
filter = '[normalize-space(@value)='+quote+String(value).trim()+quote+']';
break;
case 'text':
filter = '[normalize-space(.)='+quote+String(value).trim()+quote+']';
break;
}
this.find('./option'+filter+' | ./optgroup/option'+filter, function(error, elements){
if(error){
done(error);
}
else{
if( elements.length > 0){
elements.get( type === 'index' ? value : 0, true).click(done);
}
else{
done('<option> no found: '+value+' ('+type+')');
}
}
}).catch(done);
},
/**
* send keys to element
* @method sendKeys
* @public
* @param {String} text
* @param {Function} done callback function
*/
sendKeys(text, done){
let self = this;
let Keys = self._browser.Keys;
text = text.replace(/{(\w+)}/g, function(all, name){
let key = Keys[name.toUpperCase()];
return key?key:all;
});
self.execCmd('!sendElementKeys', {}, {
value: text.split('')
}, done);
},
/**
* get or set value
* @method val
* @public
* @param {String} [text]
* @param {Function} done callback function
*/
val(text, done){
let self = this;
if(typeof text === 'function'){
text = undefined;
}
done = getDone(arguments);
if(text){
self.clear().catch(function(){}).sendKeys(text).then(function(){
done();
}).catch(done);
}
else{
self.attr('value', done).catch(done);
}
},
/**
* send mousemove (first element)
* @method mouseMove
* @public
* @param {Number|Object} [x]
* @param {Number} [y]
* @param {Function} done callback function
*/
mouseMove(x, y, done){
let self = this;
if(typeof x === 'function'){
x = undefined;
y = undefined;
}
else if(typeof y === 'function'){
y = undefined;
}
done = getDone(arguments);
let data = {};
if(self.length > 0){
data.element = self.elementIds[0];
}
else{
return done('Elements empty: '+ self.value+' ('+self.using+')');
}
if(x !== undefined && x.x !== undefined){
y = x.y;
x = x.x;
}
if(x !== undefined && y !== undefined){
data.xoffset = x;
data.yoffset = y;
}
self.execCmd('mouseMove', {}, data, done);
},
/**
* click to element
* @method click
* @public
* @param {String} [key]
* @param {Function} done callback function
*/
click(key, done){
if(typeof key === 'function'){
key = undefined;
}
done = getDone(arguments);
if(key !== undefined){
this._browser.click(key, done);
}
else{
this.execCmd('setElementClick', done);
}
},
/**
* double click to element
* @method dblClick
* @public
* @param {Function} done callback function
*/
dblClick(done){
let self = this;
self._browser.mouseMove(self).dblClick().then(function(){
done();
}).catch(done);
},
/**
* double click to element
* @method doubleClick
* @public
* @param {Function} done callback function
*/
doubleClick(done){
let self = this;
self._browser.mouseMove(self).doubleClick().then(function(){
done();
}).catch(done);
},
/**
* drag the element drop to another element
* @method dragDropTo
* @public
* @param {Elements} selector
* @param {Number} [x]
* @param {Number} [y]
* @param {Function} done callback function
*/
dragDropTo(selector, x, y, done){
let self = this;
if(typeof x === 'function'){
x = undefined;
y = undefined;
}
done = getDone(arguments);
let to;
if(selector.selector){
to = selector;
}
else{
to = {
selector: selector,
x: x,
y: y
};
}
self._browser.dragDrop(self, to, done);
},
/**
* submit form
* @method submit
* @public
* @param {Function} done callback function
*/
submit(done){
this.execCmd('setElementSubmit', done);
},
/**
* upload file to browser machine, then set to this element
* @method uploadFile
* @public
* @param {String} localPath
* @param {Function} done callback function
*/
uploadFile(localPath, done){
let self = this;
self._browser.uploadFileToServer(localPath).then(function(tmpPath){
return self.sendKeys(tmpPath);
}).then(function(){
done();
}).catch(done);
},
/**
* scroll element to x, y
* @method scrollTo
* @public
* @param {Number|Object} [x]
* @param {Number} [y]
* @param {Function} done callback function
*/
scrollTo(x, y, done){
let self = this;
if(typeof x === 'function'){
x = undefined;
y = undefined;
}
else if(typeof y === 'function'){
y = undefined;
}
done = getDone(arguments);
let data = {};
if(self.length > 0){
data.element = self.elementIds[0];
}
else{
return done('Elements empty: '+ self.value+' ('+self.using+')');
}
if(x !== undefined && x.x !== undefined){
y = x.y;
x = x.x;
}
let script = 'function(elements, x, y){\
var element;\
for(var i=0,len=elements.length;i<len;i++){\
element = elements[i];\
element.scrollLeft = x;\
element.scrollTop = y;\
}\
}';
self._browser.exec(script, self, x, y, done);
},
/**
* touch click to element
* @method touchClick
* @public
* @param {Function} done callback function
*/
touchClick(done){
let self = this;
self.execCmd('touchClick', {}, {
element: self.elementIds[0]
}, done);
},
/**
* touch double click to element
* @method touchDblClick
* @public
* @param {Function} done callback function
*/
touchDblClick(done){
let self = this;
self.execCmd('touchDoubleClick', {}, {
element: self.elementIds[0]
}, done);
},
/**
* touch double click to element
* @method touchDoublelClick
* @public
* @param {Function} done callback function
*/
touchDoublelClick(done){
return this.touchDblClick(done);
},
/**
* touch long click to element
* @method touchLongClick
* @public
* @param {Function} done callback function
*/
touchLongClick(done){
let self = this;
self.execCmd('touchLongclick', {}, {
element: self.elementIds[0]
}, done);
},
/**
* touch scroll from element
* @method touchScroll
* @public
* @param {Number} x
* @param {Number} y
* @param {Function} done callback function
*/
touchScroll(x, y, done){
let self = this;
if(typeof x === 'function'){
x = undefined;
y = undefined;
}
else if(typeof y === 'function'){
y = undefined;
}
done = getDone(arguments);
if(x !== undefined && x.x !== undefined){
y = x.y;
x = x.x;
}
self.execCmd('touchScroll', {}, {
element: self.elementIds[0],
xoffset: x,
yoffset: y,
}, done);
},
/**
* touch flick from element
* @method touchFlick
* @public
* @param {Number} x
* @param {Number} y
* @param {Number} speed
* @param {Function} done callback function
*/
touchFlick(x, y, speed, done){
let self = this;
if(typeof x === 'function'){
x = undefined;
y = undefined;
}
else if(typeof y === 'function'){
y = undefined;
}
else if(typeof speed === 'function'){
speed = undefined;
}
done = getDone(arguments);
if(x !== undefined && x.x !== undefined){
y = x.y;
speed = x.speed;
x = x.x;
}
self.execCmd('touchFlick', {}, {
element: self.elementIds[0],
xoffset: x,
yoffset: y,
speed: speed !== undefined ? speed : 5
}, done);
},
/**
* send actions from element
* @method sendActions
* @public
* @param {String} type
* @param {Object} params
* @param {Function} done callback function
*/
sendActions(type, params, done){
let self = this;
let actions = [];
if(done === undefined){
actions = Array.isArray(type) ? type : [{
type: type
}];
}
else{
params.type = type;
actions.push(params);
}
done = getDone(arguments);
actions.map(function(action){
action.element = self.elementIds[0];
});
self.execCmd('setActions', {}, {
actions: actions
}, done);
},
/**
* find all child elements
* @method find
* @public
* @param {String} [using] find mode: class name|css selector|id|name|link text|partial link text|tag name|xpath
* @param {String} value find pattern
* @param {Function} done callback function
* @return {Elments}
*/
find(using, value, done){
let self = this;
if(typeof value === 'function'){
value = undefined;
}
done = getDone(arguments);
if(value === undefined){
// detect xpath or css selector
value = using;
using = /^\.?\//.test(value)?'xpath':'css selector';
}
self.execCmd('findChildElements', {}, {
using: using,
value: value
}, function(error, ret){
if(error){
done(error);
}
else{
let arrELEMENTS = ret.value;
let elementIds = arrELEMENTS.map(function(ELEMENT){
return ELEMENT.ELEMENT;
});
let elements = new Elements(self._browser, 'elements', elementIds);
elements.init();
done(null, elements);
}
}).catch(done);
},
/**
* test if two elements refer to the same DOM element.
* @method equal
* @public
* @param {Elements|String} elements
* @param {Function} done callback function
* @return {Boolean}
*/
equal(otherElements, done){
let self = this;
new Promise(function(resolve){
if(typeof otherElements === 'string'){
resolve(self._browser.find(otherElements));
}
else{
resolve(otherElements);
}
}).then(function(otherElements){
if(self.length === otherElements.length){
let otherElementIds = otherElements.elementIds;
let i = 0;
function testNext(){
if(i < self.length){
let elements = new Elements(self._browser, 'elements', [otherElementIds[i]]);
elements.init().execCmd('getElementEquals', {
other: otherElementIds[i]
}, function(error, ret){
if(error){
done(error);
}
else if(ret.value === false){
done(null, false);
}
else{
testNext();
}
});
}
else{
done(null, true);
}
i++;
}
testNext();
}
else{
done(null ,false);
}
}).catch(done);
}
});
// get done callback
function getDone(args){
let done = args[args.length -1];
return typeof done === 'function' ? done : null;
}
module.exports = Elements;
;