@theintern/leadfoot
Version:
Leadfoot. A JavaScript client library that brings cross-platform consistency to the Selenium WebDriver API.
725 lines • 29.6 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var findDisplayed_1 = __importDefault(require("./lib/findDisplayed"));
var fs = __importStar(require("fs"));
var Locator_1 = __importStar(require("./lib/Locator"));
var waitForDeleted_1 = __importDefault(require("./lib/waitForDeleted"));
var util_1 = require("./lib/util");
var common_1 = require("@theintern/common");
var jszip_1 = __importDefault(require("jszip"));
var path_1 = require("path");
/**
* The Element class represents a DOM or UI element within the remote
* environment.
*/
var Element = /** @class */ (function (_super) {
__extends(Element, _super);
/**
* @constructor module:leadfoot/Element
*
* @param elementId
* The ID of the element, as provided by the remote.
*
* @param session
* The session that the element belongs to.
*/
function Element(elementId, session) {
var _this = _super.call(this) || this;
_this._elementId =
elementId.ELEMENT ||
elementId.elementId ||
elementId['element-6066-11e4-a52e-4f735466cecf'] ||
elementId;
_this._session = session;
return _this;
}
Object.defineProperty(Element.prototype, "elementId", {
/**
* The opaque, remote-provided ID of the element.
*
* @member elementId
* @readonly
*/
get: function () {
return this._elementId;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Element.prototype, "session", {
/**
* The [[Session]] that the element belongs to.
* @readonly
*/
get: function () {
return this._session;
},
enumerable: false,
configurable: true
});
Element.prototype._get = function (path, requestData, pathParts) {
path = 'element/' + encodeURIComponent(this._elementId) + '/' + path;
return this._session.serverGet(path, requestData, pathParts);
};
Element.prototype._post = function (path, requestData, pathParts) {
path = 'element/' + encodeURIComponent(this._elementId) + '/' + path;
return this._session.serverPost(path, requestData, pathParts);
};
Element.prototype.toJSON = function () {
// Include both the JSONWireProtocol and W3C element properties
return {
ELEMENT: this._elementId,
'element-6066-11e4-a52e-4f735466cecf': this._elementId,
};
};
/**
* Normalize whitespace in the same way that most browsers generate
* innerText.
*
* @param text text to normalize
* @returns Text with leading and trailing whitespace removed, with inner
* runs of spaces changed to a single space, and with "\r\n" pairs
* converted to "\n".
*/
Element.prototype._normalizeWhitespace = function (text) {
if (text) {
text = text
.replace(/^\s+/gm, '')
.replace(/\s+$/gm, '')
.replace(/\s*\r\n\s*/g, '\n')
.replace(/ +/g, ' ');
}
return text;
};
/**
* Uploads a file to a remote Selenium server for use when testing file
* uploads. This API is not part of the WebDriver specification and should
* not be used directly. To send a file to a server that supports file
* uploads, use [[Element.Element.type]] to type the name of the local file
* into a file input field and the file will be transparently transmitted
* and used by the server.
*/
Element.prototype._uploadFile = function (filename) {
var _this = this;
return new common_1.Task(function (resolve) {
var content = fs.readFileSync(filename);
var zip = new jszip_1.default();
zip.file(path_1.basename(filename), content);
zip.generateAsync({ type: 'base64' }).then(function (file) {
resolve(_this.session.serverPost('file', { file: file }));
});
});
};
/**
* Gets the first element within this element that matches the given query.
*
* See [[Session.Session.setFindTimeout]] to set the amount of time it the
* remote environment should spend waiting for an element that does not
* exist at the time of the `find` call before timing out.
*
* @param using The element retrieval strategy to use. See
* [[Session.Session.find]] for options.
*
* @param value The strategy-specific value to search for. See
* [[Session.Session.find]] for details.
*/
Element.prototype.find = function (using, value) {
var session = this._session;
var capabilities = session.capabilities;
if (capabilities.usesWebDriverLocators) {
var locator = Locator_1.toW3cLocator(using, value);
using = locator.using;
value = locator.value;
}
if (using.indexOf('link text') !== -1 &&
(capabilities.brokenWhitespaceNormalization ||
capabilities.brokenLinkTextLocator)) {
return session
.execute(util_1.manualFindByLinkText, [
using,
value,
false,
this,
])
.then(function (element) {
if (!element) {
var error = new Error();
error.name = 'NoSuchElement';
throw error;
}
return new Element(element, session);
});
}
return this._post('element', {
using: using,
value: value,
})
.then(function (element) {
return new Element(element, session);
})
.catch(function (error) {
// At least Firefox 49 + geckodriver returns an UnknownCommand
// error when unable to find elements.
if (error.name === 'UnknownCommand' &&
error.message.indexOf('Unable to locate element:') !== -1) {
var newError = new Error();
newError.name = 'NoSuchElement';
newError.message = error.message;
throw newError;
}
throw error;
});
};
/**
* Gets all elements within this element that match the given query.
*
* @param using The element retrieval strategy to use. See
* [[Session.Session.find]] for options.
*
* @param value The strategy-specific value to search for. See
* [[Session.Session.find]] for details.
*/
Element.prototype.findAll = function (using, value) {
var session = this._session;
var capabilities = session.capabilities;
if (capabilities.usesWebDriverLocators) {
var locator = Locator_1.toW3cLocator(using, value);
using = locator.using;
value = locator.value;
}
var task;
if (using.indexOf('link text') !== -1 &&
(capabilities.brokenWhitespaceNormalization ||
capabilities.brokenLinkTextLocator)) {
task = session.execute(util_1.manualFindByLinkText, [
using,
value,
true,
this,
]);
}
else {
task = this._post('elements', {
using: using,
value: value,
});
}
return task.then(function (elements) {
return elements.map(function (element) {
return new Element(element, session);
});
});
};
/**
* Clicks the element. This method works on both mouse and touch platforms.
*/
Element.prototype.click = function () {
var _this = this;
if (this.session.capabilities.brokenClick) {
return (this.session
.moveMouseTo(this)
// Simulate press and release events to accompany the click
.then(function () { return _this.session.pressMouseButton(1); })
.then(function () { return _this.session.releaseMouseButton(1); })
.then(function () {
// At least in Safari 13, calling an element's click() method will not
// focus it.
return _this.session.execute(
/* istanbul ignore next */ function (element) {
element.click();
element.focus();
}, [_this]);
}));
}
return this._post('click').then(function () {
// ios-driver 0.6.6-SNAPSHOT April 2014 and MS Edge Driver 14316 do
// not wait until the default action for a click event occurs
// before returning
if (_this.session.capabilities.touchEnabled ||
_this.session.capabilities.returnsFromClickImmediately) {
return util_1.sleep(500);
}
});
};
/**
* Submits the element, if it is a form, or the form belonging to the
* element, if it is a form element.
*/
Element.prototype.submit = function () {
if (this.session.capabilities.brokenSubmitElement) {
return this.session.execute(
/* istanbul ignore next */ function (element) {
if (element.submit) {
element.submit();
}
else if (element.type === 'submit' && element.click) {
element.click();
}
}, [this]);
}
return this._post('submit');
};
/**
* Gets the visible text within the element. `<br>` elements are converted
* to line breaks in the returned text, and whitespace is normalised per
* the usual XML/HTML whitespace normalisation rules.
*/
Element.prototype.getVisibleText = function () {
var _this = this;
if (this.session.capabilities.brokenVisibleText) {
return this.session.execute(
/* istanbul ignore next */ function (element) {
return element.innerText;
}, [this]);
}
var result = this._get('text');
if (this.session.capabilities.brokenWhitespaceNormalization) {
return result.then(function (text) { return _this._normalizeWhitespace(text); });
}
return result;
};
/**
* Types into the element. This method works the same as the
* [[Session.Session.pressKeys]] method except that any modifier keys are
* automatically released at the end of the command. This method should be
* used instead of [[Session.Session.pressKeys]] to type filenames into
* file upload fields.
*
* Since 1.5, if the WebDriver server supports remote file uploads, and you
* type a path to a file on your local computer, that file will be
* transparently uploaded to the remote server and the remote filename will
* be typed instead. If you do not want to upload local files, use
* [[Session.Session.pressKeys]] instead.
*
* @param value The text to type in the remote environment. See
* [[Session.Session.pressKeys]] for more information.
*/
Element.prototype.type = function (value) {
var _this = this;
var getPostData = function (value) {
if (_this.session.capabilities.usesWebDriverElementValue) {
// At least geckodriver 0.21+ and the WebDriver standard
// require the `/value` endpoint to take a `text` parameter
// that is a string.
return { text: value.join('') };
}
else if (_this.session.capabilities.usesFlatKeysArray) {
// At least Firefox 49+ via Selenium requires the keys value to
// be a flat array of characters
return { value: value.join('').split('') };
}
else {
return { value: value };
}
};
var handleError = function (reason) {
if (reason.detail.error === 'invalid argument' &&
!_this.session.capabilities.usesWebDriverElementValue) {
_this.session.capabilities.usesWebDriverElementValue = true;
return _this.type(value);
}
throw reason;
};
if (!Array.isArray(value)) {
value = [value];
}
if (this.session.capabilities.remoteFiles) {
var filename = value.join('');
// Check to see if the input is a filename; if so, upload the file
// and then post it's remote name into the field
try {
if (fs.statSync(filename).isFile()) {
return this._uploadFile(filename).then(function (uploadedFilename) {
return _this._post('value', getPostData([uploadedFilename]))
.then(noop)
.catch(handleError);
});
}
}
catch (error) {
// ignore
}
}
// If the input isn't a filename, just post the value directly
return this._post('value', getPostData(value))
.then(noop)
.catch(handleError);
};
/**
* Gets the tag name of the element. For HTML documents, the value is
* always lowercase.
*/
Element.prototype.getTagName = function () {
var _this = this;
return this._get('name').then(function (name) {
if (_this.session.capabilities.brokenHtmlTagName) {
return _this.session
.execute('return document.body && document.body.tagName === document.body.tagName.toUpperCase();')
.then(function (isHtml) {
return isHtml ? name.toLowerCase() : name;
});
}
return name;
});
};
/**
* Clears the value of a form element.
*/
Element.prototype.clearValue = function () {
return this._post('clear').then(noop);
};
/**
* Returns whether or not a form element is currently selected (for
* drop-down options and radio buttons), or whether or not the element is
* currently checked (for checkboxes).
*/
Element.prototype.isSelected = function () {
return this._get('selected');
};
/**
* Returns whether or not a form element can be interacted with.
*/
Element.prototype.isEnabled = function () {
if (this.session.capabilities.brokenElementEnabled) {
return this.session.execute(
/* istanbul ignore next */ function (element) {
return !Boolean(element.hasAttribute('disabled'));
}, [this]);
}
return this._get('enabled');
};
/**
* Gets a property or attribute of the element according to the WebDriver
* specification algorithm. Use of this method is not recommended; instead,
* use [[Element.Element.getAttribute]] to retrieve DOM attributes and
* [[Element.Element.getProperty]] to retrieve DOM properties.
*
* This method uses the following algorithm on the server to determine what
* value to return:
*
* 1. If `name` is 'style', returns the `style.cssText` property of the
* element.
* 2. If the attribute exists and is a boolean attribute, returns 'true' if
* the attribute is true, or null otherwise.
* 3. If the element is an `<option>` element and `name` is 'value',
* returns the `value` attribute if it exists, otherwise returns the
* visible text content of the option.
* 4. If the element is a checkbox or radio button and `name` is
* 'selected', returns 'true' if the element is checked, or null
* otherwise.
* 5. If the returned value is expected to be a URL (e.g. element is `<a>`
* and attribute is `href`), returns the fully resolved URL from the
* `href`/`src` property of the element, not the attribute.
* 6. If `name` is 'class', returns the `className` property of the
* element.
* 7. If `name` is 'readonly', returns 'true' if the `readOnly` property is
* true, or null otherwise.
* 8. If `name` corresponds to a property of the element, and the property
* is not an Object, return the property value coerced to a string.
* 9. If `name` corresponds to an attribute of the element, return the
* attribute value.
*
* @param name The property or attribute name.
* @returns The value of the attribute as a string, or `null` if no such
* property or attribute exists.
*/
Element.prototype.getSpecAttribute = function (name) {
var _this = this;
return this._get('attribute/$0', null, [name])
.then(function (value) {
if (_this.session.capabilities.brokenNullGetSpecAttribute &&
(value === '' || value === undefined)) {
return _this.session
.execute(
/* istanbul ignore next */ function (element, name) {
return element.hasAttribute(name);
}, [_this, name])
.then(function (hasAttribute) {
return hasAttribute ? value : null;
});
}
return value || null;
})
.then(function (value) {
// At least ios-driver 0.6.6-SNAPSHOT violates draft spec and
// returns boolean attributes as booleans instead of the string
// "true" or null
if (typeof value === 'boolean') {
value = value ? 'true' : null;
}
return value;
});
};
/**
* Gets an attribute of the element.
*
* See [[Element.Element.getProperty]] to retrieve an element property.
*
* @param name The name of the attribute.
* @returns The value of the attribute, or `null` if no such attribute
* exists.
*/
Element.prototype.getAttribute = function (name) {
if (this.session.capabilities.usesWebDriverElementAttribute) {
return this._get('attribute/$0', null, [name]);
}
return this.session.execute('return arguments[0].getAttribute(arguments[1]);', [this, name]);
};
/**
* Gets a property of the element.
*
* See [[Element.Element.getAttribute]] to retrieve an element attribute.
*
* @param name The name of the property.
* @returns The value of the property.
*/
Element.prototype.getProperty = function (name) {
var _this = this;
if (this.session.capabilities.brokenElementProperty) {
return this.session.execute('return arguments[0][arguments[1]];', [
this,
name,
]);
}
return this._get('property/$0', null, [name]).catch(function () {
_this.session.capabilities.brokenElementProperty = true;
return _this.getProperty(name);
});
};
/**
* Determines if this element is equal to another element.
*/
Element.prototype.equals = function (other) {
var _this = this;
if (this.session.capabilities.noElementEquals) {
return this.session.execute('return arguments[0] === arguments[1];', [this, other]);
}
var elementId = other.elementId || other;
return this._get('equals/$0', null, [elementId]).catch(function (error) {
// At least Selendroid 0.9.0 does not support this command;
// At least ios-driver 0.6.6-SNAPSHOT April 2014 fails
if (!_this.session.capabilities.noElementEquals &&
(error.name === 'UnknownCommand' || error.name === 'UnknownError')) {
_this.session.capabilities.noElementEquals = true;
return _this.equals(other);
}
throw error;
});
};
/**
* Returns whether or not the element would be visible to an actual user.
* This means that the following types of elements are considered to be not
* displayed:
*
* 1. Elements with `display: none`
* 2. Elements with `visibility: hidden`
* 3. Elements positioned outside of the viewport that cannot be scrolled
* into view
* 4. Elements with `opacity: 0`
* 5. Elements with no `offsetWidth` or `offsetHeight`
*/
Element.prototype.isDisplayed = function () {
var _this = this;
return this._get('displayed').then(function (isDisplayed) {
if (isDisplayed &&
(_this.session.capabilities.brokenElementDisplayedOpacity ||
_this.session.capabilities.brokenElementDisplayedOffscreen)) {
return _this.session.execute(
/* istanbul ignore next */ function (element) {
var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
var scrollY = document.documentElement.scrollTop || document.body.scrollTop;
do {
if (window.getComputedStyle(element).opacity === '0') {
return false;
}
var bbox = element.getBoundingClientRect();
if (bbox.right + scrollX <= 0 || bbox.bottom + scrollY <= 0) {
return false;
}
} while ((element = element.parentNode) &&
element.nodeType === 1);
return true;
}, [_this]);
}
return isDisplayed;
});
};
/**
* Gets the position of the element relative to the top-left corner of the
* document, taking into account scrolling and CSS transformations (if they
* are supported).
*/
Element.prototype.getPosition = function () {
if (this.session.capabilities.brokenElementPosition) {
/* jshint browser:true */
return this.session.execute(
/* istanbul ignore next */ function (element) {
var bbox = element.getBoundingClientRect();
var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
var scrollY = document.documentElement.scrollTop || document.body.scrollTop;
return { x: scrollX + bbox.left, y: scrollY + bbox.top };
}, [this]);
}
return this._get('location').then(function (_a) {
var x = _a.x, y = _a.y;
// At least FirefoxDriver 2.41.0 incorrectly returns an object with
// additional `class` and `hCode` properties
return { x: x, y: y };
});
};
/**
* Gets the size of the element, taking into account CSS transformations
* (if they are supported).
*/
Element.prototype.getSize = function () {
var _this = this;
var getUsingExecute = function () {
return _this.session.execute(
/* istanbul ignore next */ function (element) {
var bbox = element.getBoundingClientRect();
return {
width: bbox.right - bbox.left,
height: bbox.bottom - bbox.top,
};
}, [_this]);
};
if (this.session.capabilities.brokenCssTransformedSize) {
return getUsingExecute();
}
return this._get('size')
.catch(function (error) {
// At least ios-driver 0.6.0-SNAPSHOT April 2014 does not
// support this command
if (error.name === 'UnknownCommand') {
return getUsingExecute();
}
throw error;
})
.then(function (_a) {
var width = _a.width, height = _a.height;
// At least ChromeDriver 2.9 incorrectly returns an object with
// an additional `toString` property
return { width: width, height: height };
});
};
/**
* Gets a CSS computed property value for the element.
*
* @param propertyName The CSS property to retrieve. This argument must be
* hyphenated, *not* camel-case.
*/
Element.prototype.getComputedStyle = function (propertyName) {
var _this = this;
var manualGetStyle = function () {
return _this.session.execute(
/* istanbul ignore next */ function (element, propertyName) {
return window.getComputedStyle(element)[propertyName];
}, [_this, propertyName]);
};
var promise;
if (this.session.capabilities.brokenComputedStyles) {
promise = manualGetStyle();
}
else {
promise = this._get('css/$0', null, [propertyName]).catch(function (error) {
// At least Selendroid 0.9.0 does not support this command
if (error.name === 'UnknownCommand') {
return manualGetStyle();
}
else if (error.name === 'UnknownError' &&
error.message.indexOf('failed to parse value') > -1) {
// At least ChromeDriver 2.9 incorrectly returns an error
// for property names it does not understand
return '';
}
throw error;
});
}
return promise.then(function (value) {
// At least ChromeDriver 2.9 and Selendroid 0.9.0 returns colour
// values as rgb instead of rgba
if (value) {
value = value.replace(/(.*\b)rgb\((\d+,\s*\d+,\s*\d+)\)(.*)/g, function (_, prefix, rgb, suffix) {
return prefix + 'rgba(' + rgb + ', 1)' + suffix;
});
}
// For consistency with Firefox, missing values are always returned
// as empty strings
return value != null ? value : '';
});
};
/**
* Gets the first [[Element.Element.isDisplayed|displayed]] element inside
* this element matching the given query. This is inherently slower than
* [[Element.Element.find]], so should only be used in cases where the
* visibility of an element cannot be ensured in advance.
*
* @since 1.6
*
* @param using The element retrieval strategy to use. See
* [[Session.Session.find]] for options.
*
* @param value The strategy-specific value to search for. See
* [[Session.Session.find]] for details.
*/
Element.prototype.findDisplayed = function (using, value) {
return findDisplayed_1.default(this.session, this, using, value);
};
/**
* Waits for all elements inside this element that match the given query to
* be destroyed.
*
* @param using The element retrieval strategy to use. See
* [[Session.Session.find]] for options.
*
* @param value The strategy-specific value to search for. See
* [[Session.Session.find]] for details.
*/
Element.prototype.waitForDeleted = function (strategy, value) {
return waitForDeleted_1.default(this.session, this, strategy, value);
};
return Element;
}(Locator_1.default));
exports.default = Element;
function noop() {
// At least ios-driver 0.6.6 returns an empty object for methods that are
// supposed to return no value at all, which is not correct
}
//# sourceMappingURL=Element.js.map