From 230ec88975fd1d19b3559b5afd84cec885b756b3 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 29 Nov 2018 02:41:52 -0800 Subject: [PATCH] [ftr] wrap all elements so we can swap out leadfoot without disturbing tests --- .../page_objects/visual_builder_page.js | 13 +- .../functional/page_objects/visualize_page.js | 4 +- test/functional/services/browser.js | 8 +- test/functional/services/find.js | 74 ++-- .../services/lib/element_wrapper.js | 379 ++++++++++++++++++ 5 files changed, 443 insertions(+), 35 deletions(-) create mode 100644 test/functional/services/lib/element_wrapper.js diff --git a/test/functional/page_objects/visual_builder_page.js b/test/functional/page_objects/visual_builder_page.js index 43979ea35cc2ae3..10a0645eb71eb66 100644 --- a/test/functional/page_objects/visual_builder_page.js +++ b/test/functional/page_objects/visual_builder_page.js @@ -23,6 +23,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) { const find = getService('find'); const retry = getService('retry'); const log = getService('log'); + const browser = getService('browser'); const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); const PageObjects = getPageObjects(['common', 'header', 'visualize']); @@ -64,12 +65,12 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) { // a textarea we must really select all text and remove it, and cannot use // clearValue(). if (process.platform === 'darwin') { - await input.session.pressKeys([Keys.COMMAND, 'a']); // Select all Mac + await browser.pressKeys([Keys.COMMAND, 'a']); // Select all Mac } else { - await input.session.pressKeys([Keys.CONTROL, 'a']); // Select all for everything else + await browser.pressKeys([Keys.CONTROL, 'a']); // Select all for everything else } - await input.session.pressKeys(Keys.NULL); // Release modifier keys - await input.session.pressKeys(Keys.BACKSPACE); // Delete all content + await browser.pressKeys(Keys.NULL); // Release modifier keys + await browser.pressKeys(Keys.BACKSPACE); // Delete all content await input.type(markdown); await PageObjects.header.waitUntilLoadingHasFinished(); } @@ -104,7 +105,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) { async getRhythmChartLegendValue() { const metricValue = await find.byCssSelector('.tvbLegend__itemValue'); - await metricValue.session.moveMouseTo(metricValue); + await metricValue.moveMouseTo(); return await metricValue.getVisibleText(); } @@ -207,7 +208,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) { const el = await testSubjects.find('comboBoxSearchInput'); await el.clearValue(); await el.type(timeField); - await el.session.pressKeys(Keys.RETURN); + await browser.pressKeys(Keys.RETURN); await PageObjects.header.waitUntilLoadingHasFinished(); } } diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index 7f3ccf9dceb874e..cf97bbe23c347d2 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -492,14 +492,14 @@ export function VisualizePageProvider({ getService, getPageObjects }) { const advancedLinkState = await advancedLink.getAttribute('class'); if (advancedLinkState.includes('fa-caret-right')) { - await advancedLink.session.moveMouseTo(advancedLink); + await advancedLink.moveMouseTo(); log.debug('click advancedLink'); await advancedLink.click(); } const checkbox = await find.byCssSelector('input[ng-model="axis.scale.setYExtents"]'); const checkboxState = await checkbox.getAttribute('class'); if (checkboxState.includes('ng-empty')) { - await checkbox.session.moveMouseTo(checkbox); + await checkbox.moveMouseTo(); await checkbox.click(); } const maxField = await find.byCssSelector('[ng-model="axis.scale.max"]'); diff --git a/test/functional/services/browser.js b/test/functional/services/browser.js index c464242b52226b4..d6acade0fb6d35c 100644 --- a/test/functional/services/browser.js +++ b/test/functional/services/browser.js @@ -93,8 +93,12 @@ export function BrowserProvider({ getService }) { * @param {number} yOffset Optional * @return {Promise} */ - async moveMouseTo(...args) { - await remote.moveMouseTo(...args); + async moveMouseTo(element, xOffset, yOffset) { + if (element) { + await element.moveMouseTo(); + } else { + await remote.moveMouseTo(null, xOffset, yOffset); + } } /** diff --git a/test/functional/services/find.js b/test/functional/services/find.js index 7d30b94f3b9d417..6bb9275cc1cc796 100644 --- a/test/functional/services/find.js +++ b/test/functional/services/find.js @@ -17,6 +17,8 @@ * under the License. */ +import { ElementWrapper } from './lib/element_wrapper'; + // Many of our tests use the `exists` functions to determine where the user is. For // example, you'll see a lot of code like: // if (!testSubjects.exists('someElementOnPageA')) { @@ -40,6 +42,14 @@ export function FindProvider({ getService }) { const defaultFindTimeout = config.get('timeouts.find'); + const wrap = leadfootElement => ( + new ElementWrapper(leadfootElement, remote) + ); + + const wrapAll = leadfootElements => ( + leadfootElements.map(wrap) + ); + class Find { async _withTimeout(timeout, block) { try { @@ -76,26 +86,26 @@ export function FindProvider({ getService }) { async byName(selector, timeout = defaultFindTimeout) { log.debug(`find.byName(${selector})`); return await this._ensureElementWithTimeout(timeout, async remote => { - return await remote.findByName(selector); + return wrap(await remote.findByName(selector)); }); } async byCssSelector(selector, timeout = defaultFindTimeout) { log.debug(`findByCssSelector ${selector}`); return await this._ensureElementWithTimeout(timeout, async remote => { - return await remote.findByCssSelector(selector); + return wrap(await remote.findByCssSelector(selector)); }); } async byClassName(selector, timeout = defaultFindTimeout) { log.debug(`findByCssSelector ${selector}`); return await this._ensureElementWithTimeout(timeout, async remote => { - return await remote.findByClassName(selector); + return wrap(await remote.findByClassName(selector)); }); } async activeElement() { - return await remote.getActiveElement(); + return wrap(await remote.getActiveElement()); } async setValue(selector, text) { @@ -126,57 +136,70 @@ export function FindProvider({ getService }) { async allByLinkText(selector, timeout = defaultFindTimeout) { log.debug('find.allByLinkText: ' + selector); - return await this.allByCustom(remote => remote.findAllByLinkText(selector), timeout); + return await this.allByCustom( + async remote => wrapAll(await remote.findAllByLinkText(selector)), + timeout + ); } async allByCssSelector(selector, timeout = defaultFindTimeout) { log.debug('in findAllByCssSelector: ' + selector); - return await this.allByCustom(remote => remote.findAllByCssSelector(selector), timeout); + return await this.allByCustom( + async remote => wrapAll(await remote.findAllByCssSelector(selector)), + timeout + ); } async descendantExistsByCssSelector(selector, parentElement, timeout = WAIT_FOR_EXISTS_TIME) { log.debug('Find.descendantExistsByCssSelector: ' + selector); - return await this.exists(async () => await parentElement.findDisplayedByCssSelector(selector), timeout); + return await this.exists( + async () => wrap(await parentElement.findDisplayedByCssSelector(selector)), + timeout + ); } async descendantDisplayedByCssSelector(selector, parentElement) { log.debug('Find.descendantDisplayedByCssSelector: ' + selector); - return await this._ensureElement(async () => await parentElement.findDisplayedByCssSelector(selector)); + return await this._ensureElement( + async () => wrap(await parentElement.findDisplayedByCssSelector(selector)) + ); } async allDescendantDisplayedByCssSelector(selector, parentElement) { log.debug(`Find.allDescendantDisplayedByCssSelector(${selector})`); const allElements = await parentElement.findAllByCssSelector(selector); return await Promise.all( - allElements.map((element) => this._ensureElement(async () => element)) + allElements.map((element) => ( + this._ensureElement(async () => wrap(element)) + )) ); } - async displayedByCssSelector(selector, timeout = defaultFindTimeout, parentElement) { + async displayedByCssSelector(selector, timeout = defaultFindTimeout) { log.debug('in displayedByCssSelector: ' + selector); return await this._ensureElementWithTimeout(timeout, async remote => { - return await remote.findDisplayedByCssSelector(selector); - }, parentElement); + return wrap(await remote.findDisplayedByCssSelector(selector)); + }); } async byLinkText(selector, timeout = defaultFindTimeout) { log.debug('Find.byLinkText: ' + selector); return await this._ensureElementWithTimeout(timeout, async remote => { - return await remote.findByLinkText(selector); + return wrap(await remote.findByLinkText(selector)); }); } async findDisplayedByLinkText(selector, timeout = defaultFindTimeout) { log.debug('Find.byLinkText: ' + selector); return await this._ensureElementWithTimeout(timeout, async remote => { - return await remote.findDisplayedByLinkText(selector); + return wrap(await remote.findDisplayedByLinkText(selector)); }); } async byPartialLinkText(partialLinkText, timeout = defaultFindTimeout) { log.debug(`find.byPartialLinkText(${partialLinkText})`); return await this._ensureElementWithTimeout(timeout, async remote => { - return await remote.findByPartialLinkText(partialLinkText); + return wrap(await remote.findByPartialLinkText(partialLinkText)); }); } @@ -193,26 +216,27 @@ export function FindProvider({ getService }) { async existsByLinkText(linkText, timeout = WAIT_FOR_EXISTS_TIME) { log.debug(`existsByLinkText ${linkText}`); - return await this.exists(async remote => await remote.findDisplayedByLinkText(linkText), timeout); + return await this.exists(async remote => wrap(await remote.findDisplayedByLinkText(linkText)), timeout); } async existsByDisplayedByCssSelector(selector, timeout = WAIT_FOR_EXISTS_TIME) { log.debug(`existsByDisplayedByCssSelector ${selector}`); - return await this.exists(async remote => await remote.findDisplayedByCssSelector(selector), timeout); + return await this.exists(async remote => wrap(await remote.findDisplayedByCssSelector(selector)), timeout); } async existsByCssSelector(selector, timeout = WAIT_FOR_EXISTS_TIME) { log.debug(`existsByCssSelector ${selector}`); - return await this.exists(async remote => await remote.findByCssSelector(selector), timeout); + return await this.exists(async remote => wrap(await remote.findByCssSelector(selector)), timeout); } async clickByCssSelectorWhenNotDisabled(selector, { timeout } = { timeout: defaultFindTimeout }) { log.debug(`Find.clickByCssSelectorWhenNotDisabled`); + // Don't wrap this code in a retry, or stale element checks may get caught here and the element // will never be re-grabbed. Let errors bubble, but continue checking for disabled property until // it's gone. const element = await this.byCssSelector(selector, timeout); - await remote.moveMouseTo(element); + await element.moveMouseTo(); const clickIfNotDisabled = async (element, resolve) => { const disabled = await element.getProperty('disabled'); @@ -232,7 +256,7 @@ export function FindProvider({ getService }) { log.debug(`clickByPartialLinkText(${linkText})`); await retry.try(async () => { const element = await this.byPartialLinkText(linkText, timeout); - await remote.moveMouseTo(element); + await element.moveMouseTo(); await element.click(); }); } @@ -241,7 +265,7 @@ export function FindProvider({ getService }) { log.debug(`clickByLinkText(${linkText})`); await retry.try(async () => { const element = await this.byLinkText(linkText, timeout); - await remote.moveMouseTo(element); + await element.moveMouseTo(); await element.click(); }); } @@ -257,7 +281,7 @@ export function FindProvider({ getService }) { if (index === -1) { throw new Error('Button not found'); } - return allButtons[index]; + return wrap(allButtons[index]); }); } @@ -273,7 +297,7 @@ export function FindProvider({ getService }) { log.debug(`clickByCssSelector(${selector})`); await retry.try(async () => { const element = await this.byCssSelector(selector, timeout); - await remote.moveMouseTo(element); + await element.moveMouseTo(); await element.click(); }); } @@ -281,14 +305,14 @@ export function FindProvider({ getService }) { log.debug(`clickByDisplayedLinkText(${linkText})`); await retry.try(async () => { const element = await this.findDisplayedByLinkText(linkText, timeout); - await remote.moveMouseTo(element); + await element.moveMouseTo(); await element.click(); }); } async clickDisplayedByCssSelector(selector, timeout = defaultFindTimeout) { await retry.try(async () => { const element = await this.findDisplayedByCssSelector(selector, timeout); - await remote.moveMouseTo(element); + await element.moveMouseTo(); await element.click(); }); } diff --git a/test/functional/services/lib/element_wrapper.js b/test/functional/services/lib/element_wrapper.js new file mode 100644 index 000000000000000..3d9cdb656641008 --- /dev/null +++ b/test/functional/services/lib/element_wrapper.js @@ -0,0 +1,379 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as Rx from 'rxjs'; +import { concatMap } from 'rxjs/operators'; + +function promiseProxy(promise, elementWrapper) { + const extraProps = { + then(...args) { + return promise.then(...args); + }, + catch(...args) { + return promise.catch(...args); + } + }; + + return new Proxy(elementWrapper, { + get(_, prop, receiver) { + return extraProps.hasOwnProperty(prop) + ? extraProps[prop] + : Reflect.get(elementWrapper, prop, receiver); + } + }); +} + +export class ElementWrapper { + constructor(leadfootElement, remote) { + if (leadfootElement instanceof ElementWrapper) { + return leadfootElement; + } + + const wrap = otherLeadfootElement => ( + new ElementWrapper(otherLeadfootElement, remote) + ); + + const wrapAll = otherLeadfootElements => ( + otherLeadfootElements.map(wrap) + ); + + this._queue$ = new Rx.Subject(); + this._queue$.pipe( + concatMap(async ({ defer, fn }) => { + try { + defer.resolve(await fn({ leadfootElement, remote, wrap, wrapAll })); + } catch (error) { + defer.reject(error); + } + }) + ).subscribe(); + } + + _task(fn) { + const defer = {}; + defer.promise = new Promise((resolve, reject) => { + defer.resolve = resolve; + defer.reject = reject; + }); + + this._queue$.next({ + defer, + fn + }); + + return promiseProxy(defer.promise, this); + } + + /** + * Clicks the element. This method works on both mouse and touch platforms + * https://theintern.io/leadfoot/module-leadfoot_Element.html#click + * + * @return {Promise} + */ + click() { + return this._task(async ({ leadfootElement }) => { + await leadfootElement.click(); + }); + } + + /** + * Gets all elements inside this element matching the given CSS class name. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#findAllByClassName + * + * @param {string} className + * @return {Promise} + */ + findAllByClassName(className) { + return this._task(async ({ leadfootElement, wrapAll }) => { + return wrapAll(await leadfootElement.findAllByClassName(className)); + }); + } + + /** + * Clears the value of a form element. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#clearValue + * + * @return {Promise} + */ + clearValue(...args) { + return this._task(async ({ leadfootElement }) => { + await leadfootElement.clearValue(args); + }); + } + + /** + * Types into the element. This method works the same as the leadfoot/Session#pressKeys method + * except that any modifier keys are automatically released at the end of the command. This + * method should be used instead of leadfoot/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 leadfoot/Session#pressKeys instead. + * + * @param {string|string[]} value + * @return {Promise} + */ + type(value) { + return this._task(async ({ leadfootElement }) => { + await leadfootElement.type(value); + }); + } + + /** + * Gets the first element inside this element matching the given CSS class name. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#findByClassName + * + * @param {string} className + * @return {Promise} + */ + findByClassName(className) { + return this._task(async ({ leadfootElement, wrap }) => { + return wrap(await leadfootElement.findByClassName(className)); + }); + } + + /** + * 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: + * + * - Elements with display: none + * - Elements with visibility: hidden + * - Elements positioned outside of the viewport that cannot be scrolled into view + * - Elements with opacity: 0 + * - Elements with no offsetWidth or offsetHeight + * + * https://theintern.io/leadfoot/module-leadfoot_Element.html#isDisplayed + * + * @return {Promise} + */ + isDisplayed() { + return this._task(async ({ leadfootElement }) => { + return await leadfootElement.isDisplayed(); + }); + } + + /** + * Gets an attribute of the element. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#getAttribute + * + * @param {string} name + */ + getAttribute(name) { + return this._task(async ({ leadfootElement }) => { + return await leadfootElement.getAttribute(name); + }); + } + + /** + * Gets the visible text within the element.
elements are converted to line breaks + * in the returned text, and whitespace is normalised per the usual XML/HTML whitespace + * normalisation rules. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#getVisibleText + * + * @return {Promise} + */ + getVisibleText() { + return this._task(async ({ leadfootElement }) => { + return await leadfootElement.getVisibleText(); + }); + } + + /** + * 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). + * https://theintern.io/leadfoot/module-leadfoot_Element.html#getPosition + * + * @return {Promise<{x: number, y: number}>} + */ + getPosition() { + return this._task(async ({ leadfootElement }) => { + return await leadfootElement.getPosition(); + }); + } + + /** + * Gets all elements inside this element matching the given CSS selector. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#findAllByCssSelector + * + * @param {string} selector + * @return {Promise} + */ + findAllByCssSelector(selector) { + return this._task(async ({ leadfootElement, wrapAll }) => { + return wrapAll(await leadfootElement.findAllByCssSelector(selector)); + }); + } + + /** + * Gets the first element inside this element matching the given CSS selector. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#findByCssSelector + * + * @param {string} selector + * @return {Promise} + */ + findByCssSelector(selector) { + return this._task(async ({ leadfootElement, wrap }) => { + return wrap(await leadfootElement.findByCssSelector(selector)); + }); + } + + /** + * Returns whether or not a form element can be interacted with. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#isEnabled + * + * @return {Promise} + */ + isEnabled() { + return this._task(async ({ leadfootElement }) => { + return await leadfootElement.isEnabled(); + }); + } + + /** + * Gets all elements inside this element matching the given HTML tag name. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#findAllByTagName + * + * @param {string} tagName + * @return {Promise} + */ + findAllByTagName(tagName) { + return this._task(async ({ leadfootElement, wrapAll }) => { + return wrapAll(await leadfootElement.findAllByTagName(tagName)); + }); + } + + /** + * Gets a property of the element. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#getProperty + * + * @param {string} name + * @return {Promise} + */ + getProperty(name) { + return this._task(async ({ leadfootElement }) => { + return await leadfootElement.getProperty(name); + }); + } + + /** + * Moves the remote environment’s mouse cursor to this element. If the element is outside + * of the viewport, the remote driver will attempt to scroll it into view automatically. + * https://theintern.io/leadfoot/module-leadfoot_Session.html#moveMouseTo + * + * @return {Promise} + */ + moveMouseTo() { + return this._task(async ({ leadfootElement, remote }) => { + await remote.moveMouseTo(leadfootElement); + }); + } + + /** + * Gets a CSS computed property value for the element. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#getComputedStyle + * + * @param {string} propertyName + * @return {Promise} + */ + getComputedStyle(propertyName) { + return this._task(async ({ leadfootElement }) => { + return await leadfootElement.getComputedStyle(propertyName); + }); + } + + /** + * Gets the size of the element, taking into account CSS transformations (if they are supported). + * https://theintern.io/leadfoot/module-leadfoot_Element.html#getSize + * + * @return {Promise<{width: number, height: number}>} + */ + getSize() { + return this._task(async ({ leadfootElement }) => { + return await leadfootElement.getSize(); + }); + } + + /** + * Gets the first element inside this element matching the given HTML tag name. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#findByTagName + * + * @param {string} tagName + * @return {Promise} + */ + findByTagName(tagName) { + return this._task(async ({ leadfootElement, wrap }) => { + return wrap(await leadfootElement.findByTagName(tagName)); + }); + } + + /** + * 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). + * https://theintern.io/leadfoot/module-leadfoot_Element.html#isSelected + * + * @return {Promise} + */ + isSelected() { + return this._task(async ({ leadfootElement }) => { + return await leadfootElement.isSelected(); + }); + } + + /** + * Gets the first displayed element inside this element matching the given CSS selector. This is + * inherently slower than leadfoot/Element#find, so should only be used in cases where the + * visibility of an element cannot be ensured in advance. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#findDisplayedByCssSelector + * + * @param {string} selector + * @return {Promise} + */ + findDisplayedByCssSelector(selector) { + return this._task(async ({ leadfootElement, wrap }) => { + return wrap(await leadfootElement.findDisplayedByCssSelector(selector)); + }); + } + + /** + * Waits for all elements inside this element matching the given CSS class name to be destroyed. + * https://theintern.io/leadfoot/module-leadfoot_Element.html#waitForDeletedByClassName + * + * @param {string} className + * @return {Promise} + */ + waitForDeletedByClassName(className) { + return this._task(async ({ leadfootElement }) => { + await leadfootElement.waitForDeletedByClassName(className); + }); + } + + /** + * https://theintern.io/leadfoot/module-leadfoot_Element.html#findByXpath + * @deprecated + * @param {string} xpath + * @return {Promise} + */ + findByXpath(xpath) { + return this._task(async ({ leadfootElement, wrap }) => { + return wrap(await leadfootElement.findByXpath(xpath)); + }); + } +}