"use strict"; /** * Creates a new instance of `Element`. * * `Element` binds DOM element in browser context with virtual element in test. * * @class * @name Element * @arg {string} name - Element name. * @arg {object} selector - CSS selector of DOM element. * @arg {Page} page - Page with element. */ const _ = require("lodash"); var weak = require("weak"); var U = require("glace-utils"); var CONF = require("../config"); var PointerEvents = require("./event"); const utils = require("../utils"); var Element = function (name, selector, page) { this.name = name; this.selector = selector; this._event = new PointerEvents(selector, page); this._page = weak(page); this._elCmd = utils.getElementCommand(selector); this._elsCmd = utils.getElementsCommand(selector); this._webdriver = null; }; /** * Helper to get webdriver. * * @method * @return {object} webdriver instance */ Element.prototype._getDriver = function () { if (!this._webdriver) { this._webdriver = this._page.getDriver(); }; return this._webdriver; }; /** * Gets webdriver element. * * @async * @method * @return {Promise<object>} Webdriver element. */ Element.prototype.getElement = function () { return this._getDriver().element(this.selector); }; /** * Gets webdriver elements. * * @async * @method * @return {Promise<array<object>>} Webdriver elements. */ Element.prototype.getElements = function () { return this._getDriver().elements(this.selector); }; /** * Gets count of elements in browser. * * @async * @method * @return {Promise<integer>} Count of elements */ Element.prototype.getCount = async function () { const cmd = `return ${this._elsCmd}.length`; return this._getDriver().execute(cmd).then(result => result.value); }; /** * Gets text content of DOM element. * * @async * @method * @arg {object} [opts] - Options. * @arg {boolean} [opts.now=false] - Make it immediately. * @arg {?number} [opts.timeout] - Time to wait for element visibility, sec. * @return {Promise<string>} Text value or null. */ Element.prototype.getText = async function (opts) { opts = U.defVal(opts, {}); var now = U.defVal(opts.now, false); if (!now) { await this.waitForVisible(opts.timeout); }; var value = await this._getDriver().getText(this.selector); if (!_.isEmpty(value)) return utils.clearElementText(value); value = await this._getDriver().getAttribute(this.selector, "value"); if (!_.isEmpty(value)) return utils.clearElementText(value); value = await this._getDriver().getAttribute(this.selector, "innerHTML"); if (!_.isEmpty(value)) return utils.clearElementText(value); return null; }; /** * Sets text to DOM element. * * @async * @method * @arg {string} text - Text value to assign. * @arg {object} [opts] - Options. * @arg {boolean} [opts.now=false] - Make it immediately. * @arg {?number} [opts.timeout] - Time to wait for element visibility, sec. * @arg {boolean} [opts.scroll=true] - Scroll to element. * @arg {boolean} [opts.enter=false] - Send `Enter` after text. * @return {Promise} */ Element.prototype.setText = async function (text, opts) { opts = U.defVal(opts, {}); var now = U.defVal(opts.now, false); var scroll = U.defVal(opts.scroll, true); const enter = U.defVal(opts.enter, false); if (scroll) { await this.scrollIntoView(opts); } else if (!now) { await this.waitForVisible(opts.timeout); }; if (enter) text += "\n"; await this._getDriver().setValue(this.selector, text); }; /** * Gets DOM element location with attributes: * `x`, `y`, `midX`, `midY`, `width`, `height`. * * @async * @method * @return {Promise<object>} Location of element. */ Element.prototype.location = function () { var cmd = ` \ var ctrl = ${this._elCmd}; \ var rect = ctrl.getBoundingClientRect(); \ var location = {}; \ location.x = Math.ceil(rect.left); \ location.y = Math.ceil(rect.top); \ location.width = Math.ceil(rect.width); \ location.height = Math.ceil(rect.height); \ location.midX = Math.ceil(location.x + location.width / 2); \ location.midY = Math.ceil(location.y + location.height / 2); \ return location;`; return this._getDriver().execute(cmd).then(result => result.value); }; /** * Scrolls element into browser viewport. * * @async * @method * @arg {object} [opts] - Options. * @arg {boolean} [opts.now=false] - Make it immediately. * @arg {?number} [opts.timeout] - Time to wait for element visibility, sec. * @return {Promise} */ Element.prototype.scrollIntoView = async function (opts) { opts = U.defVal(opts, {}); var now = U.defVal(opts.now, false); if (!now) { await this.waitForVisible(opts.timeout); }; var cmd = "return " + this._elCmd + ".scrollIntoView();"; await this._getDriver().execute(cmd); }; /** * Clicks element in browser. * * @async * @method * @arg {object} [opts] - Options. * @arg {boolean} [opts.now=false] - Make it immediately. * @arg {?number} [opts.timeout] - Time to wait for element visibility, sec. * @arg {boolean} [opts.scroll=true] - Scroll to element. * @return {Promise} */ Element.prototype.click = async function (opts) { opts = U.defVal(opts, {}); var now = U.defVal(opts.now, false); var scroll = U.defVal(opts.scroll, true); if (scroll) { await this.scrollIntoView(opts); } else if (!now) { await this.waitForVisible(opts.timeout); }; await this._getDriver().click(this.selector); }; /** * Clicks element in browser via pointer events. * * @async * @method * @arg {object} [opts] - Options. * @arg {boolean} [opts.now=false] - Make it immediately. * @arg {?number} [opts.timeout] - Time to wait for element visibility, sec. * @arg {boolean} [opts.scroll=true] - Scroll to element. * @return {Promise} */ Element.prototype.pClick = async function (opts) { opts = U.defVal(opts, {}); var now = U.defVal(opts.now, false); var scroll = U.defVal(opts.scroll, true); if (scroll) { await this.scrollIntoView(opts); } else if (!now) { await this.waitForVisible(opts.timeout); }; var loc = await this.location(); await this._event.move(loc.midX, loc.midY); await this._event.down(loc.midX, loc.midY); await this._event.up(loc.midX, loc.midY); }; /** * Taps element in browser. * * @async * @method * @arg {object} [opts] - Options. * @arg {boolean} [opts.now=false] - Make it immediately. * @arg {?number} [opts.timeout] - Time to wait for element visibility, sec. * @arg {boolean} [opts.scroll=true] - Scroll to element. * @return {Promise} */ Element.prototype.tap = Element.prototype.click; /** * Defines whether element is selected or no. * * @async * @method * @arg {object} [opts] - Options. * @arg {boolean} [opts.now=false] - Make it immediately. * @arg {?number} [opts.timeout] - Time to wait for element visibility, sec. * @return {Promise<boolean>} `true` if element is selected, `false` otherwise. */ Element.prototype.isSelected = async function (opts) { opts = U.defVal(opts, {}); var now = U.defVal(opts.now, false); if (!now) { await this.waitForVisible(opts.timeout); }; var result = await this._getDriver().isSelected(this.selector); if (result) return true; result = await this._getDriver().getAttribute(this.selector, "class"); return result.includes("selected"); }; /** * Defines whether element is exist or no. * * @async * @method * @return {Promise<boolean>} `true` if element is exist, `false` otherwise. */ Element.prototype.isExist = function () { var cmd = "return !!" + this._elCmd + ";"; return this._getDriver().execute(cmd).then(result => result.value); }; /** * Waits for element is exist. * * @async * @method * @arg {number} [timeout] - Timeout to wait, sec. * @return {Promise} * @throws {TimeoutError} If element doesn't exist after timeout. */ Element.prototype.waitForExist = function (timeout) { timeout = U.defVal(timeout, CONF.web.uiTimeout) * 1000; var errMsg = `${this.name} (${this.selector}) doesn't exist after ${timeout}s`; return this._getDriver().waitUntil(async () => { return await this.isExist(); }, timeout, errMsg); }; /** * Waits for element isn't exist. * * @async * @method * @arg {number} [timeout] - Timeout to wait, sec. * @return {Promise} * @throws {TimeoutError} If element is still exist after timeout. */ Element.prototype.waitForNonExist = function (timeout) { timeout = U.defVal(timeout, CONF.web.uiTimeout) * 1000; var errMsg = `${this.name} (${this.selector}) still exists after ${timeout}s`; return this._getDriver().waitUntil(async () => { return !(await this.isExist()); }, timeout, errMsg); }; /** * Defines whether element is visible or no. * * @method * @return {Promise<boolean>} `true` if element is visible, `false` otherwise. */ Element.prototype.isVisible = function () { return this.isExist().then(result => { if (!result) return false; return this._getDriver().isVisible(this.selector); }); }; /** * Waits for element is visible. * * @async * @method * @arg {number} [timeout] - Timeout to wait, sec. * @return {Promise} * @throws {TimeoutError} If element isn't visible after timeout. */ Element.prototype.waitForVisible = function (timeout) { timeout = U.defVal(timeout, CONF.web.uiTimeout) * 1000; var errMsg = `${this.name} (${this.selector}) isn't visible ` + `after ${timeout}s`; return this._getDriver().waitUntil(async () => { return await this.isVisible(); }, timeout, errMsg); }; /** * Waits for element is invisible. * * @async * @method * @arg {number} [timeout] - Timeout to wait, sec. * @return {Promise} * @throws {TimeoutError} If element is still visible after timeout. */ Element.prototype.waitForInvisible = function (timeout) { timeout = U.defVal(timeout, CONF.web.uiTimeout) * 1000; var errMsg = `${this.name} (${this.selector}) is still visible after ${timeout}s`; return this._getDriver().waitUntil(async () => { return !(await this.isVisible()); }, timeout, errMsg); }; /** * Clones element with custom properties. * * @async * @method * @arg {object} [opts] - Clone options. * @arg {string} [opts.name] - Element name. * @arg {object} [opts.selector] - CSS selector of DOM element. * @arg {Page} [opts.page] - Page with element. * @return {Element} Cloned element. */ Element.prototype.clone = function (opts = {}) { const name = U.defVal(opts.name, this.name); const selector = U.defVal(opts.selector, this.selector); const page = U.defVal(opts.page, weak.get(this._page)); return new this.constructor(name, selector, page); }; module.exports = Element;