UNPKG

@theintern/leadfoot

Version:

Leadfoot. A JavaScript client library that brings cross-platform consistency to the Selenium WebDriver API.

725 lines 29.6 kB
"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