chrominator
Version:
High Level automation framework for chrome
512 lines (470 loc) • 11.7 kB
JavaScript
'use strict'
var Keyboard = require('./keyboard').Keyboard
const debug = require('debug')('chrominator.node')
/**
* Abstract DOM Element
* @module Node
*/
/**
* @constructor
* @param {Driver} driver
* @param {string} nodeId - The nodes node id
*/
var Node = function (driver, nodeId) {
this.driver = driver
this.crd = this.driver.crd
this.nodeId = nodeId
this.DOM = this.crd.DOM
this.Input = this.crd.Input
this.Runtime = this.crd.Runtime
this.objectGroup = 'global'
}
/**
* Add the remote object id to the Node
*
* @private
*/
Node.prototype.init = function () {
debug('init')
const self = this
return self.toRemoteObject().then((result) => {
Object.defineProperty(self, 'objectId', {
value: result.object.objectId,
writable: false
})
return self
})
}
// raw get attributes
Node.prototype._getAttributes = function () {
return this.DOM.getAttributes({nodeId: this.nodeId})
}
/**
* Get the Node's attributes.
*
* @example
* attributes = await node.getAttributes()
*
* @return {Object} The attributes as key-value pairs.
*
*/
Node.prototype.getAttributes = function () {
return this._getAttributes().then((result) => {
const attrs = result['attributes']
const len = attrs.length
const data = {}
for (var i = 0; i < len; i++) {
data[attrs[i]] = attrs[++i]
}
return data
})
}
/**
* Get an attribute on the node.
*
* @example
* value = await node.getAttribute('class')
*
* @param {string} name - attribute name
* @return {(string|null)} attribute value or null if the attribute does not exist
*
*/
Node.prototype.getAttribute = function (name) {
return this._getAttributes().then((result) => {
const attrs = result['attributes']
const len = attrs.length
for (var i = 0; i < len; i++) {
if (attrs[i] === name) {
return attrs[i + 1]
}
}
return null
})
}
/**
* Search for a descendent of the current Node.
*
* @example
* node = await node.querySelector({selector: '#my-id'})
* node = await node.querySelector('#my-id')
*
* @param {(Object|string)} args - selector arguments or selector
* @return {Node}
*
*/
Node.prototype.querySelector = function (args) {
if (typeof args === 'string') {
args = {selector: args}
}
args['nodeId'] = this.nodeId
return this.driver.querySelector(args)
}
/**
* Search for descendents of the current Node.
*
* @example
* nodes = await node.querySelectorAll({selector: 'a'})
* nodes = await node.querySelectorAll('a')
*
* @param {(Object|string)} args - selector arguments or selector
* @return {Array.<Node>}
*
*/
Node.prototype.querySelectorAll = function (args) {
if (typeof args === 'string') {
args = {selector: args}
}
args['nodeId'] = this.nodeId
return this.driver.querySelectorAll(args)
}
/**
* Set file selection on a file input element.
*
* @example
* node.setFileInput({files: ['/opt/my-file.txt']})
*
* @param {Object} args
*
*/
Node.prototype.setFileInput = function (args) {
args.nodeId = this.nodeId
return this.DOM.setFileInputFiles(args)
}
/**
* Focus on the Node
*
* @example
* node.focus()
*
*/
Node.prototype.focus = function () {
return this.DOM.focus({nodeId: this.nodeId})
}
/**
* Test if the Node is clickable at a given location.
*
* @param {Object} args
* @return {boolean} true if the element will directly receive a click event, otherwise false
*
*/
Node.prototype.clickableAt = function (args) {
return this.equal(this.DOM.getNodeForLocation(args))
}
/**
* Resolve Node at default click point
*
* @param {Object} args
* @return {Node} Node to directly receive click
*
*/
Node.prototype.resolveNodeAtDefaultClickPoint = function () {
const self = this
return self.getClickCoords().then((result) => {
return self.DOM.getNodeForLocation(result)
}).then((result) => {
return new Node(self.driver, result.nodeId).init()
})
}
/**
* Determine if the Node will receive a click
*
* @param {Object} args
* @return {boolean}
*
*/
Node.prototype.isClickable = function () {
const self = this
return self.resolveNodeAtDefaultClickPoint().then((result) => {
return self.evaluate({
functionDeclaration: function () {
const self = this
let node = arguments[0]
while (node) {
if (self.isSameNode(node)) {
return true
}
node = node.parentNode
}
return false
},
args: [result]
})
})
}
/**
* Calculate coordinates at the center of the Node for the click event.
*
* @return {{x:Number, y:Number}} coordinates for the click event.
*
*/
Node.prototype.getClickCoords = function () {
return this.DOM.getBoxModel({nodeId: this.nodeId}).then((result) => {
const contentQuad = result.model.content
return {
x: Math.round((contentQuad[0] + contentQuad[2]) / 2),
y: Math.round((contentQuad[1] + contentQuad[5]) / 2)
}
})
}
/**
* Click on the Node.
*
* @example
* node.click()
*
* @param {Object}
*
*/
Node.prototype.click = function (args) {
var options = args || {}
options.offset = options.offset || {}
var xOffset = options.offset.x || 0
var yOffset = options.offset.y || 0
var coords
const self = this
return this.getClickCoords().then((result) => {
coords = result
return self.Input.dispatchMouseEvent({
type: 'mousePressed',
x: coords.x + xOffset,
y: coords.y + yOffset,
modifiers: options.modifiers || 0,
button: options.button || 'left',
clickCount: options.clickCount || 1
})
}).then(() => {
return self.Input.dispatchMouseEvent({
type: 'mouseReleased',
x: coords.x,
y: coords.y,
modifiers: options.modifiers || 0,
button: options.button || 'left',
clickCount: options.clickCount || 1
})
})
}
/**
* Hover on the Node.
*
* @example
* node.hover()
*
* @param {Object}
*/
Node.prototype.hover = function (args) {
var options = args || {}
var coords
var self = this
return this.getClickCoords().then((result) => {
coords = result
return self.Input.dispatchMouseEvent({
type: 'mouseMoved',
x: coords.x,
y: coords.y,
modifiers: options.modifiers || 0,
button: options.button || 'none',
clickCount: options.clickCount || 0
})
})
}
/**
* Type text to the Node
*
* @example
* node.sendKeys('jesg')
*
* @param {string} text - text to type into the node
*/
Node.prototype.sendKeys = function (text) {
var self = this
return this.focus().then(() => {
return self.getAttribute('type')
}).then((elemType) => {
if (elemType === 'file') {
return self.setFileInput({files: text.split('\n')})
} else {
var keyboard = new Keyboard(text)
const keyEvents = keyboard.toKeyEvents().map(function (event) {
return self.Input.dispatchKeyEvent(event)
})
return Promise.all(keyEvents)
}
})
}
/**
* Set a property on the Node
*
* @example
* node.setProperty('value', 'jesg')
*
* @param {string} name - properties name
* @param {string} value - properties value
*/
Node.prototype.setProperty = function (name, value) {
const self = this
return self.evaluate({
functionDeclaration: 'this[arguments[0]] = arguments[1]',
args: [name, value]
})
}
/**
* @private
*
* Resolve the Node's remote object id.
*/
Node.prototype.toRemoteObject = function () {
const self = this
return self.DOM.resolveNode({nodeId: self.nodeId})
}
/**
*
* Get a property on the Node
*
* @example
* value = await node.getProperty('value')
*
* @param {string} name - properties name
* @return {string} properties value
*/
Node.prototype.getProperty = function (name) {
return this.getProperties().then((properties) => {
return properties[name]
})
}
/**
*
* Get the Node's properties
*
* @example
* properties = await node.getProperties()
*
* @return {Object} properties as key-value pairs
*/
Node.prototype.getProperties = function () {
const self = this
var obj
return self.toRemoteObject().then((result) => {
obj = result.object
return self.Runtime.getProperties({
objectId: obj.objectId,
ownProperties: false,
accessorPropertiesOnly: true
})
}).then((result) => {
if (result.exceptionDetails) {
return Promise.reject(new Error(JSON.stringify(result.exceptionDetails)))
}
const properties = result.result
const len = properties.length
const types = ['string', 'number', 'boolean']
const data = {}
for (var i = 0; i < len; i++) {
const property = properties[i]
if (typeof (property.value) !== 'undefined' && types.indexOf(property.value.type) !== -1) {
data[property.name] = property.value.value
}
}
return self.Runtime.releaseObject({objectId: obj.objectId}).then(() => {
return data
})
})
}
/**
*
* Evaluate javascript in the context of this Node.
*
* @example
* propertyValue = await node.evaluate({
* functionDeclaration: function(name) {
* return this[name];
* },
* args: [name],
* });
*
*
* @param {Object} options
* @return {Object} resolved object
*/
Node.prototype.evaluate = function (options) {
const self = this
var scriptTimeout = Number.parseInt(options.timeout || self.driver.timeouts.script)
var str = options.functionDeclaration
if (typeof str !== 'function') {
str = `function() {
${str}
}`
}
str = str.toString().trim()
options.functionDeclaration = `function() { const args = arguments; const self = this; return new Promise((resolve,reject) => {
try {
setTimeout(function() { reject(new Error('Chrominator Script Timeout')); }, ${scriptTimeout});
resolve(function() {
return ${str}
}().apply(self, args));
} catch(err) {
reject(err);
}
}); }`
options.objectId = self.objectId
return self.driver._evaluate(options)
}
/**
*
* Evaluate Asynchronous javascript in the context of this Node.
*
* The `functionDeclaration` must call either `resolve` to resolve the promise or `reject` to reject the promise.
*
* @param {Object} options
* @return {Object} resolved object
*/
Node.prototype.evaluateAsync = function (options) {
const self = this
var scriptTimeout = Number.parseInt(options.timeout || self.driver.timeouts.script)
var str = options.functionDeclaration
if (typeof str !== 'function') {
str = `function() {
${str}
}`
}
str = str.toString().trim()
options.functionDeclaration = `function() { const args = arguments; const self = this; return new Promise((resolve,reject) => {
try {
setTimeout(function() { reject(new Error('Chrominator Script Timeout')); }, ${scriptTimeout});
(function() {
return ${str}
}().apply(self, args));
} catch(err) {
reject(err);
}
}); }`
options.objectId = self.objectId
return self.driver._evaluate(options)
}
/**
* Test if the Node is equal to another Node.
*
* @param {Node} node - The Node to test against
* @return {boolean}
*/
Node.prototype.equal = function (node) {
const self = this
// nodeId is not a sufficient test.
return self.evaluate({
functionDeclaration: 'return this.isSameNode(arguments[0])',
args: [node]
})
}
/**
* Get visible text.
*
* The current implementation does not clean whitespace.
*
* @return {string}
*/
Node.prototype.text = function (node) {
const self = this
return self.evaluate({
functionDeclaration: 'return this.innerText'
})
}
module.exports = Node