diff --git a/.eslintrc.js b/.eslintrc.js index 3d5be9fa46501..ea7490c59ee2e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -208,7 +208,7 @@ module.exports = { */ { files: [ - 'test/functional/services/lib/leadfoot_element_wrapper/scroll_into_view_if_necessary.js', + 'test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js', '**/browser_exec_scripts/**/*', ], rules: { diff --git a/package.json b/package.json index 2bd402674c47e..8dd063b4478bd 100644 --- a/package.json +++ b/package.json @@ -375,7 +375,6 @@ "karma-junit-reporter": "1.2.0", "karma-mocha": "1.3.0", "karma-safari-launcher": "1.0.0", - "leadfoot": "1.7.5", "license-checker": "^16.0.0", "listr": "^0.14.1", "load-grunt-config": "0.19.2", @@ -394,6 +393,7 @@ "proxyquire": "1.7.11", "regenerate": "^1.4.0", "sass-lint": "^1.12.1", + "selenium-webdriver": "^4.0.0-alpha.1", "simple-git": "1.37.0", "sinon": "^5.0.7", "strip-ansi": "^3.0.1", diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 3093af827d70e..4509af6ac5555 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -33,6 +33,7 @@ export default { '/src/setup_node_env', '/packages', '/src/test_utils', + '/test/functional/services/remote', ], collectCoverageFrom: [ 'packages/kbn-ui-framework/src/components/**/*.js', diff --git a/src/functional_test_runner/lib/providers/provider_collection.js b/src/functional_test_runner/lib/providers/provider_collection.js index dba2466c9868e..259b762c1f207 100644 --- a/src/functional_test_runner/lib/providers/provider_collection.js +++ b/src/functional_test_runner/lib/providers/provider_collection.js @@ -105,7 +105,7 @@ export class ProviderCollection { instance = createAsyncInstance(type, name, instance); } - if (name !== '__leadfoot__' && name !== 'log' && name !== 'config' && instance && typeof instance === 'object') { + if (name !== '__webdriver__' && name !== 'log' && name !== 'config' && instance && typeof instance === 'object') { instance = createVerboseInstance( this._log, type === 'PageObject' ? `PageObjects.${name}` : name, diff --git a/test/functional/apps/dashboard/_dashboard_grid.js b/test/functional/apps/dashboard/_dashboard_grid.js index fe7d3be5f27ba..6aacd82003bc4 100644 --- a/test/functional/apps/dashboard/_dashboard_grid.js +++ b/test/functional/apps/dashboard/_dashboard_grid.js @@ -37,10 +37,9 @@ export default function ({ getService, getPageObjects }) { const lastVisTitle = 'Rendering Test: datatable'; const panelTitleBeforeMove = await dashboardPanelActions.getPanelHeading(lastVisTitle); const position1 = await panelTitleBeforeMove.getPosition(); - await browser.dragAndDrop( - { element: panelTitleBeforeMove }, - { element: null, xOffset: -20, yOffset: -450 } + { location: panelTitleBeforeMove }, + { location: { x: -20, y: -450 } } ); const panelTitleAfterMove = await dashboardPanelActions.getPanelHeading(lastVisTitle); diff --git a/test/functional/apps/dashboard/_dashboard_save.js b/test/functional/apps/dashboard/_dashboard_save.js index 0b93733e36157..31b155916607a 100644 --- a/test/functional/apps/dashboard/_dashboard_save.js +++ b/test/functional/apps/dashboard/_dashboard_save.js @@ -41,7 +41,7 @@ export default function ({ getPageObjects }) { await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName); + await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName, { waitDialogIsClosed: false }); await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true }); }); @@ -55,7 +55,7 @@ export default function ({ getPageObjects }) { it('Saves on confirm duplicate title warning', async function () { await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName); + await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName, { waitDialogIsClosed: false }); await PageObjects.dashboard.clickSave(); @@ -96,7 +96,7 @@ export default function ({ getPageObjects }) { it('Warns when case is different', async function () { await PageObjects.dashboard.switchToEditMode(); - await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName.toUpperCase()); + await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName.toUpperCase(), { waitDialogIsClosed: false }); await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true }); diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index cd3d34cdde009..67e6ba0fe73fb 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -307,9 +307,9 @@ export function DashboardPageProvider({ getService, getPageObjects }) { * verify that the save was successful * * @param dashName {String} - * @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean, needsConfirm: false}} + * @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean, needsConfirm: false, waitDialogIsClosed: boolean }} */ - async saveDashboard(dashName, saveOptions = {}) { + async saveDashboard(dashName, saveOptions = { waitDialogIsClosed: true }) { await this.enterDashboardTitleAndClickSave(dashName, saveOptions); if (saveOptions.needsConfirm) { @@ -352,12 +352,11 @@ export function DashboardPageProvider({ getService, getPageObjects }) { /** * * @param dashboardTitle {String} - * @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean}} + * @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean, waitDialogIsClosed: boolean}} */ - async enterDashboardTitleAndClickSave(dashboardTitle, saveOptions = {}) { + async enterDashboardTitleAndClickSave(dashboardTitle, saveOptions = { waitDialogIsClosed: true }) { await testSubjects.click('dashboardSaveMenuItem'); - - await PageObjects.header.waitUntilLoadingHasFinished(); + const modalDialog = await testSubjects.find('savedObjectSaveModal'); log.debug('entering new title'); await testSubjects.setValue('savedObjectTitle', dashboardTitle); @@ -371,6 +370,9 @@ export function DashboardPageProvider({ getService, getPageObjects }) { } await this.clickSave(); + if (saveOptions.waitDialogIsClosed) { + await testSubjects.waitForDeleted(modalDialog); + } } async selectDashboard(dashName) { diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 9d9741fc57e56..3c2f36df21823 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -117,8 +117,8 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { async brushHistogram(from, to) { const bars = await find.allByCssSelector('.series.histogram rect'); await browser.dragAndDrop( - { element: bars[from], xOffset: 0, yOffset: -5 }, - { element: bars[to], xOffset: 0, yOffset: -5 } + { location: bars[from], offset: { x: 0, y: -5 } }, + { location: bars[to], offset: { x: 0, y: -5 } } ); } diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index 938aa22fbcc4d..fa2be9eb9f28e 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -332,7 +332,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { log.debug(`setIndexPatternField(${indexPatternName})`); const field = await this.getIndexPatternField(); await field.clearValue(); - await field.type(indexPatternName); + await field.type(indexPatternName, { charByChar: true }); const currentName = await field.getAttribute('value'); log.debug(`setIndexPatternField set to ${currentName}`); expect(currentName).to.eql(`${indexPatternName}${expectWildcard ? '*' : ''}`); diff --git a/test/functional/page_objects/share_page.js b/test/functional/page_objects/share_page.js index b94d5c2de3ee4..777482b55771b 100644 --- a/test/functional/page_objects/share_page.js +++ b/test/functional/page_objects/share_page.js @@ -19,6 +19,7 @@ export function SharePageProvider({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); + const find = getService('find'); const PageObjects = getPageObjects(['visualize', 'common']); const log = getService('log'); @@ -43,8 +44,9 @@ export function SharePageProvider({ getService, getPageObjects }) { // and then re-open the menu await this.clickShareTopNavButton(); } - - return testSubjects.click(`sharePanel-${itemTitle.replace(' ', '')}`); + const menuPanel = await find.byCssSelector('div.euiContextMenuPanel'); + testSubjects.click(`sharePanel-${itemTitle.replace(' ', '')}`); + await testSubjects.waitForDeleted(menuPanel); } async getSharedUrl() { diff --git a/test/functional/page_objects/time_picker.js b/test/functional/page_objects/time_picker.js index 212b772000c93..ef57503236600 100644 --- a/test/functional/page_objects/time_picker.js +++ b/test/functional/page_objects/time_picker.js @@ -39,6 +39,14 @@ export function TimePickerPageProvider({ getService, getPageObjects }) { return dateString.substring(0, 23); } + async getTimePickerPanel() { + return await find.byCssSelector('div.euiPopover__panel-isOpen'); + } + + async waitPanelIsGone(panelElement) { + await find.waitForElementStale(panelElement); + } + /** * @param {String} fromTime YYYY-MM-DD HH:mm:ss.SSS * @param {String} fromTime YYYY-MM-DD HH:mm:ss.SSS @@ -49,11 +57,14 @@ export function TimePickerPageProvider({ getService, getPageObjects }) { // set to time await testSubjects.click('superDatePickerendDatePopoverButton'); + let panel = await this.getTimePickerPanel(); await testSubjects.click('superDatePickerAbsoluteTab'); await testSubjects.setValue('superDatePickerAbsoluteDateInput', toTime); // set from time await testSubjects.click('superDatePickerstartDatePopoverButton'); + await this.waitPanelIsGone(panel); + panel = await this.getTimePickerPanel(); await testSubjects.click('superDatePickerAbsoluteTab'); await testSubjects.setValue('superDatePickerAbsoluteDateInput', fromTime); @@ -68,6 +79,7 @@ export function TimePickerPageProvider({ getService, getPageObjects }) { await testSubjects.click('querySubmitButton'); } + await this.waitPanelIsGone(panel); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } @@ -145,11 +157,13 @@ export function TimePickerPageProvider({ getService, getPageObjects }) { // get to time await testSubjects.click('superDatePickerendDatePopoverButton'); + const panel = await this.getTimePickerPanel(); await testSubjects.click('superDatePickerAbsoluteTab'); const end = await testSubjects.getAttribute('superDatePickerAbsoluteDateInput', 'value'); // get from time await testSubjects.click('superDatePickerstartDatePopoverButton'); + await this.waitPanelIsGone(panel); await testSubjects.click('superDatePickerAbsoluteTab'); const start = await testSubjects.getAttribute('superDatePickerAbsoluteDateInput', 'value'); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 3e9ff28ff8980..bd2f219486bf5 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -80,7 +80,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro await input.pressKeys([browser.keys.CONTROL, 'a']); // Select all for everything else } await input.pressKeys(browser.keys.NULL); // Release modifier keys - await input.pressKeys(browser.keys.BACKSPACE); // Delete all content + await input.pressKeys(browser.keys.BACK_SPACE); // Delete all content } public async getMarkdownText() { diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index 8e9648c2a252e..7e7726a050405 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -738,13 +738,11 @@ export function VisualizePageProvider({ getService, getPageObjects }) { async ensureSavePanelOpen() { log.debug('ensureSavePanelOpen'); - let isOpen = await testSubjects.exists('savedObjectSaveModal'); - await retry.try(async () => { - while (!isOpen) { - await testSubjects.click('visualizeSaveButton'); - isOpen = await testSubjects.exists('savedObjectSaveModal'); - } - }); + await PageObjects.header.waitUntilLoadingHasFinished(); + const isOpen = await testSubjects.exists('savedObjectSaveModal', { timeout: 5000 }); + if (!isOpen) { + await testSubjects.click('visualizeSaveButton'); + } } async saveVisualization(vizName, { saveAsNew = false } = {}) { diff --git a/test/functional/services/browser.js b/test/functional/services/browser.js index cab68187b4089..cac147f2f0ec4 100644 --- a/test/functional/services/browser.js +++ b/test/functional/services/browser.js @@ -20,53 +20,49 @@ import { cloneDeep } from 'lodash'; import { modifyUrl } from '../../../src/core/utils'; -import Keys from 'leadfoot/keys'; +import { WebElementWrapper } from './lib/web_element_wrapper'; -import { LeadfootElementWrapper } from './lib/leadfoot_element_wrapper'; - -export function BrowserProvider({ getService }) { - const leadfoot = getService('__leadfoot__'); +export async function BrowserProvider({ getService }) { + const { driver, Key, LegacyActionSequence } = await getService('__webdriver__').init(); class BrowserService { /** * Keyboard events */ - keys = Keys; + keys = Key; /** - * Gets the dimensions of a window. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#getWindowSize + * Retrieves the a rect describing the current top-level window's size and position. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Window.html * - * @param {string} windowHandle Optional - Omit this argument to query the currently focused window. - * @return {Promise<{width: number, height: number}>} + * @return {Promise<{height: number, width: number, x: number, y: number}>} */ - async getWindowSize(...args) { - return await leadfoot.getWindowSize(...args); + async getWindowSize() { + return await driver.manage().window().getRect(); } /** * Sets the dimensions of a window. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#setWindowSize + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Window.html * - * @param {string} windowHandle Optional * @param {number} width * @param {number} height * @return {Promise} */ async setWindowSize(...args) { - await leadfoot.setWindowSize(...args); + await driver.manage().window().setRect({ width: args[0], height: args[1] }); } /** * Gets the URL that is loaded in the focused window/frame. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#getCurrentUrl + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getCurrentUrl * * @return {Promise} */ async getCurrentUrl() { // strip _t=Date query param when url is read - const current = await leadfoot.getCurrentUrl(); + const current = await driver.getCurrentUrl(); const currentWithoutTime = modifyUrl(current, parsed => { delete parsed.query._t; }); @@ -75,7 +71,7 @@ export function BrowserProvider({ getService }) { /** * Navigates the focused window/frame to a new URL. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#get + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Driver.html#get * * @param {string} url * @param {boolean} insertTimestamp Optional @@ -87,187 +83,243 @@ export function BrowserProvider({ getService }) { parsed.query._t = Date.now(); }); - return await leadfoot.get(urlWithTime); + return await driver.get(urlWithTime); } - return await leadfoot.get(url); + return await driver.get(url); } /** * Moves the remote environment’s mouse cursor to the specified element or relative - * position. 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 + * position. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move * - * @param {Element} element Optional + * @param {WebElementWrapper} element Optional * @param {number} xOffset Optional * @param {number} yOffset Optional * @return {Promise} */ async moveMouseTo(element, xOffset, yOffset) { - if (element) { - await element.moveMouseTo(xOffset, yOffset); + const mouse = driver.actions().mouse(); + const actions = driver.actions({ bridge: true }); + if (element instanceof WebElementWrapper) { + await actions.pause(mouse).move({ origin: element._webElement }).perform(); + } else if (isNaN(xOffset) || isNaN(yOffset) === false) { + await actions.pause(mouse).move({ origin: { x: xOffset, y: yOffset } }).perform(); } else { - await leadfoot.moveMouseTo(null, xOffset, yOffset); + throw new Error('Element or coordinates should be provided'); } } /** * Does a drag-and-drop action from one point to another + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#dragAndDrop * - * @param {{element: LeadfootElementWrapper, xOffset: number, yOffset: number}} from - * @param {{element: LeadfootElementWrapper, xOffset: number, yOffset: number}} to + * @param {{element: WebElementWrapper | {x: number, y: number}, offset: {x: number, y: number}}} from + * @param {{element: WebElementWrapper | {x: number, y: number}, offset: {x: number, y: number}}} to * @return {Promise} */ async dragAndDrop(from, to) { - await this.moveMouseTo(from.element, from.xOffset, from.yOffset); - await leadfoot.pressMouseButton(); - await this.moveMouseTo(to.element, to.xOffset, to.yOffset); - await leadfoot.releaseMouseButton(); + let _from; + let _to; + const _fromOffset = (from.offset) ? { x: from.offset.x || 0, y: from.offset.y || 0 } : { x: 0, y: 0 }; + const _toOffset = (to.offset) ? { x: to.offset.x || 0, y: to.offset.y || 0 } : { x: 0, y: 0 }; + if (from.location instanceof WebElementWrapper) { + _from = from.location._webElement; + } else { + _from = from.location; + } + + if (to.location instanceof WebElementWrapper) { + _to = to.location._webElement; + } else { + _to = to.location; + } + + if (from.location instanceof WebElementWrapper && typeof to.location.x === 'number') { + const actions = driver.actions({ bridge: true }); + return await actions + .move({ origin: _from }) + .press() + .move({ x: _to.x, y: _to.y, origin: 'pointer' }) + .release() + .perform(); + } else { + return await new LegacyActionSequence(driver) + .mouseMove(_from, _fromOffset) + .mouseDown() + .mouseMove(_to, _toOffset) + .mouseUp() + .perform(); + } } /** * Reloads the current browser window/frame. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#refresh + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#refresh * * @return {Promise} */ async refresh() { - await leadfoot.refresh(); + await driver.navigate().refresh(); } /** * Navigates the focused window/frame back one page using the browser’s navigation history. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#goBack + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#back * * @return {Promise} */ async goBack() { - await leadfoot.goBack(); + await driver.navigate().back(); } /** - * Types into the focused window/frame/element. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#pressKeys + * Sends a sequance of keyboard keys. For each key, this will record a pair of keyDown and keyUp actions + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#sendKeys * * @param {string|string[]} keys * @return {Promise} */ async pressKeys(...args) { - await leadfoot.pressKeys(...args); + const actions = driver.actions({ bridge: true }); + const chord = this.keys.chord(...args); + await actions.sendKeys(chord).perform(); } /** - * Clicks a mouse button at the point where the mouse cursor is currently positioned. This - * method may fail to execute with an error if the mouse has not been moved anywhere since - * the page was loaded. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#clickMouseButton + * Inserts an action for moving the mouse x and y pixels relative to the specified origin. + * The origin may be defined as the mouse's current position, the viewport, or the center + * of a specific WebElement. Then adds an action for left-click (down/up) with the mouse. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#click * - * @param {number} button Optional + * @param {WebElementWrapper} element Optional + * @param {number} xOffset Optional + * @param {number} yOffset Optional * @return {Promise} */ async clickMouseButton(...args) { - await leadfoot.clickMouseButton(...args); + const mouse = driver.actions().mouse(); + const actions = driver.actions({ bridge: true }); + if (args[0] instanceof WebElementWrapper) { + await actions.pause(mouse).move({ origin: args[0]._webElement }).click().perform(); + } else if (isNaN(args[1]) || isNaN(args[2]) === false) { + await actions.pause(mouse).move({ origin: { x: args[1], y: args[2] } }).click().perform(); + } else { + throw new Error('Element or coordinates should be provided'); + } } /** * Gets the HTML loaded in the focused window/frame. This markup is serialised by the remote * environment so may not exactly match the HTML provided by the Web server. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#getPageSource + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getPageSource * * @return {Promise} */ - async getPageSource(...args) { - return await leadfoot.getPageSource(...args); + async getPageSource() { + return await driver.getPageSource(); } /** * Gets all logs from the remote environment of the given type. The logs in the remote * environment are cleared once they have been retrieved. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#getLogsFor + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Logs.html#get * - * @param {string} type + * @param {!logging.Type} type The desired log type. * @return {Promise} */ async getLogsFor(...args) { - return await leadfoot.getLogsFor(...args); + //The logs endpoint has not been defined in W3C Spec browsers other than Chrome don't have access to this endpoint. + //See: https://github.com/w3c/webdriver/issues/406 + //See: https://w3c.github.io/webdriver/#endpoints + if (driver.executor_.w3c === true) { + return []; + } else { + return await driver.manage().logs().get(...args); + } } /** - * Gets a screenshot of the focused window and returns it in PNG format. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#takeScreenshot + * Gets a screenshot of the focused window and returns it as a base-64 encoded PNG + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#takeScreenshot * * @return {Promise} */ - async takeScreenshot(...args) { - return await leadfoot.takeScreenshot(...args); + async takeScreenshot() { + return await driver.takeScreenshot(); } /** - * Double-clicks the primary mouse button. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#doubleClick - * + * Inserts action for performing a double left-click with the mouse. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#doubleClick + * @param {WebElementWrapper} element * @return {Promise} */ - async doubleClick(...args) { - await leadfoot.doubleClick(...args); + async doubleClick(element) { + const actions = driver.actions({ bridge: true }); + if (element instanceof WebElementWrapper) { + await actions.doubleClick(element._webElement).perform(); + } else { + await actions.doubleClick().perform(); + } } /** - * Switches the currently focused window to a new window. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#switchToWindow + * Changes the focus of all future commands to another window. Windows may be specified + * by their window.name attributeor by its handle (as returned by WebDriver#getWindowHandles). + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_TargetLocator.html * * @param {string} handle * @return {Promise} */ async switchToWindow(...args) { - await leadfoot.switchToWindow(...args); + await driver.switchTo().window(...args); } /** * Gets a list of identifiers for all currently open windows. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#getAllWindowHandles + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getAllWindowHandles * * @return {Promise} */ - async getAllWindowHandles(...args) { - return await leadfoot.getAllWindowHandles(...args); + async getAllWindowHandles() { + return await driver.getAllWindowHandles(); } /** * Sets a value in local storage for the focused window/frame. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#setLocalStorageItem * * @param {string} key * @param {string} value * @return {Promise} */ async setLocalStorageItem(key, value) { - await leadfoot.setLocalStorageItem(key, value); + await driver.executeScript('return window.localStorage.setItem(arguments[0], arguments[1]);', key, value); } /** * Closes the currently focused window. In most environments, after the window has been * closed, it is necessary to explicitly switch to whatever window is now focused. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#closeCurrentWindow + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#close * * @return {Promise} */ - async closeCurrentWindow(...args) { - await leadfoot.closeCurrentWindow(...args); + async closeCurrentWindow() { + await driver.close(); } /** * Executes JavaScript code within the focused window/frame. The code should return a value synchronously. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#execute + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#executeScript * * @param {string|function} function * @param {...any[]} args */ async execute(fn, ...args) { - return await leadfoot.execute(fn, cloneDeep(args, arg => { - if (arg instanceof LeadfootElementWrapper) { - return arg._leadfootElement; + return await driver.executeScript(fn, ...cloneDeep(args, arg => { + if (arg instanceof WebElementWrapper) { + return arg._webElement; } })); } diff --git a/test/functional/services/combo_box.js b/test/functional/services/combo_box.js index 11881a4f3ff69..b60556bacfd94 100644 --- a/test/functional/services/combo_box.js +++ b/test/functional/services/combo_box.js @@ -85,16 +85,16 @@ export function ComboBoxProvider({ getService }) { async getComboBoxSelectedOptions(comboBoxSelector) { log.debug(`comboBox.getComboBoxSelectedOptions, comboBoxSelector: ${comboBoxSelector}`); - const comboBox = await testSubjects.find(comboBoxSelector); - const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill'); - if (selectedOptions.length === 0) { - return []; - } - - const getOptionValuePromises = selectedOptions.map(async (optionElement) => { - return await optionElement.getVisibleText(); + return await retry.try(async () => { + const comboBox = await testSubjects.find(comboBoxSelector); + const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill'); + if (selectedOptions.length === 0) { + return []; + } + return Promise.all(selectedOptions.map(async (optionElement) => { + return await optionElement.getVisibleText(); + })); }); - return await Promise.all(getOptionValuePromises); } async clear(comboBoxSelector) { diff --git a/test/functional/services/filter_bar.js b/test/functional/services/filter_bar.js index 1ef95fc367b3f..51b3de354bc82 100644 --- a/test/functional/services/filter_bar.js +++ b/test/functional/services/filter_bar.js @@ -26,7 +26,10 @@ export function FilterBarProvider({ getService, getPageObjects }) { hasFilter(key, value, enabled = true) { const filterActivationState = enabled ? 'enabled' : 'disabled'; return testSubjects.exists( - `filter & filter-key-${key} & filter-value-${value} & filter-${filterActivationState}` + `filter & filter-key-${key} & filter-value-${value} & filter-${filterActivationState}`, + { + allowHidden: true + } ); } diff --git a/test/functional/services/find.js b/test/functional/services/find.js index 30164ce9d5b52..f16765fd03f7e 100644 --- a/test/functional/services/find.js +++ b/test/functional/services/find.js @@ -17,86 +17,62 @@ * under the License. */ -import { LeadfootElementWrapper } from './lib/leadfoot_element_wrapper'; +import { WebElementWrapper } from './lib/web_element_wrapper'; -export function FindProvider({ getService }) { +export async function FindProvider({ getService }) { const log = getService('log'); const config = getService('config'); - const leadfoot = getService('__leadfoot__'); + const webdriver = await getService('__webdriver__').init(); const retry = getService('retry'); + const driver = webdriver.driver; + const By = webdriver.By; + const until = webdriver.until; + const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists'); const defaultFindTimeout = config.get('timeouts.find'); const fixedHeaderHeight = config.get('layout.fixedHeaderHeight'); - const wrap = leadfootElement => ( - new LeadfootElementWrapper(leadfootElement, leadfoot, fixedHeaderHeight) + const wrap = webElement => ( + new WebElementWrapper(webElement, webdriver, defaultFindTimeout, fixedHeaderHeight, log) ); - const wrapAll = leadfootElements => ( - leadfootElements.map(wrap) + const wrapAll = webElements => ( + webElements.map(wrap) ); class Find { - async _withTimeout(timeout, block) { - try { - const leadfootWithTimeout = leadfoot.setFindTimeout(timeout); - return await block(leadfootWithTimeout); - } finally { - leadfoot.setFindTimeout(defaultFindTimeout); - } - } - async _ensureElement(getElementFunction) { - return await retry.try(async () => { - const element = await getElementFunction(); - // Calling any method forces a staleness check - await element.isEnabled(); - return element; - }); - } + currentWait = defaultFindTimeout; - async _ensureElementWithTimeout(timeout, getElementFunction) { - try { - const leadfootWithTimeout = leadfoot.setFindTimeout(timeout); - return await retry.try(async () => { - const element = await getElementFunction(leadfootWithTimeout); - // Calling any method forces a staleness check - await element.isEnabled(); - return element; - }); - } finally { - leadfoot.setFindTimeout(defaultFindTimeout); + async _withTimeout(timeout) { + if (timeout !== this.currentWait) { + this.currentWait = timeout; + await driver.manage().setTimeouts({ implicit: timeout }); } } async byName(selector, timeout = defaultFindTimeout) { - log.debug(`find.byName(${selector})`); - return await this._ensureElementWithTimeout(timeout, async leadfoot => { - return wrap(await leadfoot.findByName(selector)); - }); + log.debug(`Find.byName('${selector}') with timeout=${timeout}`); + return wrap(await driver.wait(until.elementLocated(By.name(selector)), timeout)); } async byCssSelector(selector, timeout = defaultFindTimeout) { - log.debug(`findByCssSelector ${selector}`); - return await this._ensureElementWithTimeout(timeout, async leadfoot => { - return wrap(await leadfoot.findByCssSelector(selector)); - }); + log.debug(`Find.findByCssSelector('${selector}') with timeout=${timeout}`); + return wrap(await driver.wait(until.elementLocated(By.css(selector)), timeout)); } async byClassName(selector, timeout = defaultFindTimeout) { - log.debug(`findByCssSelector ${selector}`); - return await this._ensureElementWithTimeout(timeout, async leadfoot => { - return wrap(await leadfoot.findByClassName(selector)); - }); + log.debug(`Find.findByClassName('${selector}') with timeout=${timeout}`); + return wrap(await driver.wait(until.elementLocated(By.className(selector)), timeout)); } async activeElement() { - return wrap(await leadfoot.getActiveElement()); + return wrap(await driver.switchTo().activeElement()); } async setValue(selector, text) { - log.debug(`find.setValue(${selector}, ${text})`); + log.debug(`Find.setValue('${selector}', '${text}')`); return await retry.try(async () => { const element = await this.byCssSelector(selector); await element.click(); @@ -105,143 +81,156 @@ export function FindProvider({ getService }) { // call clearValue() and type() on the element that is focused after // clicking on the testSubject const input = await this.activeElement(); - await input.clearValue(); - await input.type(text); + if (input) { + await input.clearValue(); + await input.type(text); + } else { + await element.clearValue(); + await element.type(text); + } }); } + async filterElementIsDisplayed(elements) { + if (elements.length === 0) { + return []; + } else { + const displayed = []; + for (let i = 0; i < elements.length; i++) { + const isDisplayed = await elements[i].isDisplayed(); + if (isDisplayed) { + displayed.push(elements[i]); + } + } + return displayed; + } + } + async allByCustom(findAllFunction, timeout = defaultFindTimeout) { - return await this._withTimeout(timeout, async leadfoot => { - return await retry.try(async () => { - let elements = await findAllFunction(leadfoot); - if (!elements) elements = []; - // Force isStale checks for all the retrieved elements. - await Promise.all(elements.map(async element => await element.isEnabled())); - return elements; - }); + await this._withTimeout(timeout); + return await retry.try(async () => { + let elements = await findAllFunction(driver); + if (!elements) elements = []; + // Force isStale checks for all the retrieved elements. + await Promise.all(elements.map(async element => await element.isEnabled())); + await this._withTimeout(defaultFindTimeout); + return elements; }); } async allByLinkText(selector, timeout = defaultFindTimeout) { - log.debug('find.allByLinkText: ' + selector); - return await this.allByCustom( - async leadfoot => wrapAll(await leadfoot.findAllByLinkText(selector)), - timeout - ); + log.debug(`Find.allByLinkText('${selector}') with timeout=${timeout}`); + await this._withTimeout(timeout); + const elements = await driver.findElements(By.linkText(selector)); + await this._withTimeout(defaultFindTimeout); + return wrapAll(elements); } async allByCssSelector(selector, timeout = defaultFindTimeout) { - log.debug('in findAllByCssSelector: ' + selector); - return await this.allByCustom( - async leadfoot => wrapAll(await leadfoot.findAllByCssSelector(selector)), - timeout - ); + log.debug(`Find.allByCssSelector('${selector}') with timeout=${timeout}`); + await this._withTimeout(timeout); + const elements = await driver.findElements(By.css(selector)); + await this._withTimeout(defaultFindTimeout); + return wrapAll(elements); } async descendantExistsByCssSelector(selector, parentElement, timeout = WAIT_FOR_EXISTS_TIME) { - log.debug('Find.descendantExistsByCssSelector: ' + selector); - return await this.exists( - async () => wrap(await parentElement.findDisplayedByCssSelector(selector)), - timeout - ); + log.debug(`Find.descendantExistsByCssSelector('${selector}') with timeout=${timeout}`); + return await this.exists(async () => wrapAll(await parentElement._webElement.findElements(By.css(selector)), timeout)); } async descendantDisplayedByCssSelector(selector, parentElement) { - log.debug('Find.descendantDisplayedByCssSelector: ' + selector); - return await this._ensureElement( - async () => wrap(await parentElement.findDisplayedByCssSelector(selector)) - ); + log.debug(`Find.descendantDisplayedByCssSelector('${selector}')`); + const element = await parentElement._webElement.findElement(By.css(selector)); + const descendant = wrap(element); + const isDisplayed = await descendant.isDisplayed(); + if (isDisplayed) { + return descendant; + } else { + throw new Error('Element is not displayed'); + } } 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 () => wrap(element)) - )) - ); + log.debug(`Find.allDescendantDisplayedByCssSelector('${selector}')`); + const allElements = await wrapAll(await parentElement._webElement.findElements(By.css(selector))); + return await this.filterElementIsDisplayed(allElements); } - async displayedByCssSelector(selector, timeout = defaultFindTimeout) { - log.debug('in displayedByCssSelector: ' + selector); - return await this._ensureElementWithTimeout(timeout, async leadfoot => { - return wrap(await leadfoot.findDisplayedByCssSelector(selector)); - }); + async displayedByLinkText(linkText, timeout = defaultFindTimeout) { + log.debug(`Find.displayedByLinkText('${linkText}') with timeout=${timeout}`); + const element = await this.byLinkText(linkText, timeout); + log.debug(`Wait for element become visible: ${linkText} with timeout=${timeout}`); + await driver.wait(until.elementIsVisible(element._webElement), timeout); + return wrap(element); } - async byLinkText(selector, timeout = defaultFindTimeout) { - log.debug('Find.byLinkText: ' + selector); - return await this._ensureElementWithTimeout(timeout, async leadfoot => { - return wrap(await leadfoot.findByLinkText(selector)); - }); + async displayedByCssSelector(selector, timeout = defaultFindTimeout) { + log.debug(`Find.displayedByCssSelector(${selector})`); + const element = await this.byCssSelector(selector, timeout); + log.debug(`Wait for element become visible: ${selector} with timeout=${timeout}`); + await driver.wait(until.elementIsVisible(element._webElement), timeout); + return wrap(element); } - async findDisplayedByLinkText(selector, timeout = defaultFindTimeout) { - log.debug('Find.byLinkText: ' + selector); - return await this._ensureElementWithTimeout(timeout, async leadfoot => { - return wrap(await leadfoot.findDisplayedByLinkText(selector)); - }); + async byLinkText(selector, timeout = defaultFindTimeout) { + log.debug(`Find.byLinkText('${selector}') with timeout=${timeout}`); + return wrap(await driver.wait(until.elementLocated(By.linkText(selector)), timeout)); } async byPartialLinkText(partialLinkText, timeout = defaultFindTimeout) { - log.debug(`find.byPartialLinkText(${partialLinkText})`); - return await this._ensureElementWithTimeout(timeout, async leadfoot => { - return wrap(await leadfoot.findByPartialLinkText(partialLinkText)); - }); + log.debug(`Find.byPartialLinkText('${partialLinkText}') with timeout=${timeout}`); + return wrap(await driver.wait(until.elementLocated(By.partialLinkText(partialLinkText)), timeout)); } async exists(findFunction, timeout = WAIT_FOR_EXISTS_TIME) { - return await this._withTimeout(timeout, async leadfoot => { - try { - await findFunction(leadfoot); - return true; - } catch (error) { - return false; + await this._withTimeout(timeout); + try { + const found = await findFunction(driver); + await this._withTimeout(defaultFindTimeout); + if (Array.isArray(found)) { + return found.length > 0; + } else { + return found instanceof WebElementWrapper; } - }); + } catch (err) { + await this._withTimeout(defaultFindTimeout); + return false; + } } async existsByLinkText(linkText, timeout = WAIT_FOR_EXISTS_TIME) { - log.debug(`existsByLinkText ${linkText}`); - return await this.exists(async leadfoot => wrap(await leadfoot.findDisplayedByLinkText(linkText)), timeout); + log.debug(`Find.existsByLinkText('${linkText}') with timeout=${timeout}`); + return await this.exists(async driver => wrapAll(await driver.findElements(By.linkText(linkText))), timeout); } async existsByDisplayedByCssSelector(selector, timeout = WAIT_FOR_EXISTS_TIME) { - log.debug(`existsByDisplayedByCssSelector ${selector}`); - return await this.exists(async leadfoot => wrap(await leadfoot.findDisplayedByCssSelector(selector)), timeout); + log.debug(`Find.existsByDisplayedByCssSelector('${selector}') with timeout=${timeout}`); + return await this.exists(async (driver) => { + const elements = wrapAll(await driver.findElements(By.css(selector))); + return await this.filterElementIsDisplayed(elements); + }, timeout); } async existsByCssSelector(selector, timeout = WAIT_FOR_EXISTS_TIME) { - log.debug(`existsByCssSelector ${selector}`); - return await this.exists(async leadfoot => wrap(await leadfoot.findByCssSelector(selector)), timeout); + log.debug(`Find.existsByCssSelector('${selector}') with timeout=${timeout}`); + return await this.exists(async driver => wrapAll(await driver.findElements(By.css(selector))), timeout); } async clickByCssSelectorWhenNotDisabled(selector, { timeout } = { timeout: defaultFindTimeout }) { - log.debug(`Find.clickByCssSelectorWhenNotDisabled`); + log.debug(`Find.clickByCssSelectorWhenNotDisabled('${selector}') with timeout=${timeout}`); // 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 element.moveMouseTo(); - - const clickIfNotDisabled = async (element, resolve) => { - const disabled = await element.getProperty('disabled'); - if (disabled) { - log.debug('Element is disabled, try again'); - setTimeout(() => clickIfNotDisabled(element, resolve), 250); - } else { - await element.click(); - resolve(); - } - }; - - await new Promise(resolve => clickIfNotDisabled(element, resolve)); + await driver.wait(until.elementIsEnabled(element._webElement), timeout); + await element.click(); } async clickByPartialLinkText(linkText, timeout = defaultFindTimeout) { - log.debug(`clickByPartialLinkText(${linkText})`); + log.debug(`Find.clickByPartialLinkText('${linkText}') with timeout=${timeout}`); await retry.try(async () => { const element = await this.byPartialLinkText(linkText, timeout); await element.moveMouseTo(); @@ -250,7 +239,7 @@ export function FindProvider({ getService }) { } async clickByLinkText(linkText, timeout = defaultFindTimeout) { - log.debug(`clickByLinkText(${linkText})`); + log.debug(`Find.clickByLinkText('${linkText}') with timeout=${timeout}`); await retry.try(async () => { const element = await this.byLinkText(linkText, timeout); await element.moveMouseTo(); @@ -258,10 +247,11 @@ export function FindProvider({ getService }) { }); } - async byButtonText(buttonText, element = leadfoot, timeout = defaultFindTimeout) { - log.debug(`byButtonText(${buttonText})`); + async byButtonText(buttonText, element = driver, timeout = defaultFindTimeout) { + log.debug(`Find.byButtonText('${buttonText}') with timeout=${timeout}`); return await retry.tryForTime(timeout, async () => { - const allButtons = await element.findAllByTagName('button'); + const _element = (element instanceof WebElementWrapper) ? element._webElement : element; + const allButtons = wrapAll(await _element.findElements(By.tagName('button'))); const buttonTexts = await Promise.all(allButtons.map(async (el) => { return el.getVisibleText(); })); @@ -269,12 +259,12 @@ export function FindProvider({ getService }) { if (index === -1) { throw new Error('Button not found'); } - return wrap(allButtons[index]); + return allButtons[index]; }); } - async clickByButtonText(buttonText, element = leadfoot, timeout = defaultFindTimeout) { - log.debug(`clickByButtonText(${buttonText})`); + async clickByButtonText(buttonText, element = driver, timeout = defaultFindTimeout) { + log.debug(`Find.clickByButtonText('${buttonText}') with timeout=${timeout}`); await retry.try(async () => { const button = await this.byButtonText(buttonText, element, timeout); await button.click(); @@ -282,37 +272,63 @@ export function FindProvider({ getService }) { } async clickByCssSelector(selector, timeout = defaultFindTimeout) { - log.debug(`clickByCssSelector(${selector})`); + log.debug(`Find.clickByCssSelector('${selector}') with timeout=${timeout}`); await retry.try(async () => { const element = await this.byCssSelector(selector, timeout); - await element.moveMouseTo(); - await element.click(); + if (element) { + //await element.moveMouseTo(); + await element.click(); + } else { + throw new Error(`Element with css='${selector}' is not found`); + } }); } async clickByDisplayedLinkText(linkText, timeout = defaultFindTimeout) { - log.debug(`clickByDisplayedLinkText(${linkText})`); + log.debug(`Find.clickByDisplayedLinkText('${linkText}') with timeout=${timeout}`); await retry.try(async () => { - const element = await this.findDisplayedByLinkText(linkText, timeout); - await element.moveMouseTo(); - await element.click(); + const element = await this.displayedByLinkText(linkText, timeout); + if (element) { + await element.moveMouseTo(); + await element.click(); + } else { + throw new Error(`Element with linkText='${linkText}' is not found`); + } }); } async clickDisplayedByCssSelector(selector, timeout = defaultFindTimeout) { + log.debug(`Find.clickDisplayedByCssSelector('${selector}') with timeout=${timeout}`); await retry.try(async () => { - const element = await this.findDisplayedByCssSelector(selector, timeout); - await element.moveMouseTo(); - await element.click(); + const element = await this.displayedByCssSelector(selector, timeout); + if (element) { + await element.moveMouseTo(); + await element.click(); + } else { + throw new Error(`Element with css='${selector}' is not found`); + } }); } - async waitForDeletedByCssSelector(selector) { - await leadfoot.waitForDeletedByCssSelector(selector); + async waitForDeletedByCssSelector(selector, timeout = defaultFindTimeout) { + log.debug(`Find.waitForDeletedByCssSelector('${selector}') with timeout=${timeout}`); + await driver.wait(async () => { + const found = await driver.findElements(By.css(selector)); + return found.length === 0; + }, + timeout, + `The element ${selector} was still present when it should have disappeared.`); } + async waitForAttributeToChange(selector, attribute, value) { + log.debug(`Find.waitForAttributeToChange('${selector}', '${attribute}', '${value}')`); retry.waitFor(`${attribute} to equal "${value}"`, async () => { const el = await this.byCssSelector(selector); return value === await el.getAttribute(attribute); }); } + + async waitForElementStale(element, timeout = defaultFindTimeout) { + log.debug(`Find.waitForElementStale with timeout=${timeout}`); + await driver.wait(until.stalenessOf(element._webElement), timeout); + } } return new Find(); diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 941a46e3fb474..8ba5c95c307b4 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -63,7 +63,7 @@ import { PieChartProvider } from './visualizations'; import { VisualizeListingTableProvider } from './visualize_listing_table'; export const services = { - __leadfoot__: RemoteProvider, + __webdriver__: RemoteProvider, filterBar: FilterBarProvider, queryBar: QueryBarProvider, find: FindProvider, diff --git a/test/functional/services/lib/leadfoot_element_wrapper/index.js b/test/functional/services/lib/leadfoot_element_wrapper/index.js deleted file mode 100644 index 8569f9a2e120c..0000000000000 --- a/test/functional/services/lib/leadfoot_element_wrapper/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -export { LeadfootElementWrapper } from './leadfoot_element_wrapper'; diff --git a/test/functional/services/lib/leadfoot_element_wrapper/leadfoot_element_wrapper.js b/test/functional/services/lib/leadfoot_element_wrapper/leadfoot_element_wrapper.js deleted file mode 100644 index 8886c0233777e..0000000000000 --- a/test/functional/services/lib/leadfoot_element_wrapper/leadfoot_element_wrapper.js +++ /dev/null @@ -1,372 +0,0 @@ -/* - * 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 { scrollIntoViewIfNecessary } from './scroll_into_view_if_necessary'; -import cheerio from 'cheerio'; -import testSubjSelector from '@kbn/test-subj-selector'; - -export class LeadfootElementWrapper { - constructor(leadfootElement, leadfoot, fixedHeaderHeight) { - if (leadfootElement instanceof LeadfootElementWrapper) { - return leadfootElement; - } - - this._leadfootElement = leadfootElement; - this._leadfoot = leadfoot; - this._fixedHeaderHeight = fixedHeaderHeight; - } - - _wrap(otherLeadfootElement) { - return new LeadfootElementWrapper(otherLeadfootElement, this._leadfoot, this._fixedHeaderHeight); - } - - _wrapAll(otherLeadfootElements) { - return otherLeadfootElements.map(e => this._wrap(e)); - } - - /** - * Clicks the element. This method works on both mouse and touch platforms - * https://theintern.io/leadfoot/module-leadfoot_Element.html#click - * - * @return {Promise} - */ - async click() { - await this.scrollIntoViewIfNecessary(); - await this._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} - */ - async findAllByClassName(className) { - return await this._wrapAll( - await this._leadfootElement.findAllByClassName(className) - ); - } - - /** - * Clears the value of a form element. - * https://theintern.io/leadfoot/module-leadfoot_Element.html#clearValue - * - * @return {Promise} - */ - async clearValue() { - await this._leadfootElement.clearValue(); - } - - /** - * 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} - */ - async type(value) { - await this._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} - */ - async findByClassName(className) { - return this._wrap(await this._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} - */ - async isDisplayed() { - return await this._leadfootElement.isDisplayed(); - } - - /** - * Gets an attribute of the element. - * https://theintern.io/leadfoot/module-leadfoot_Element.html#getAttribute - * - * @param {string} name - */ - async getAttribute(name) { - return await this._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} - */ - async getVisibleText() { - return await this._leadfootElement.getVisibleText(); - } - - /** - * Gets the tag name of the element. For HTML documents, the value is always lowercase. - * https://theintern.io/leadfoot/module-leadfoot_Element.html#getTagName - * - * @return {Promise} - */ - async getTagName() { - return await this._leadfootElement.getTagName(); - } - - /** - * 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}>} - */ - async getPosition() { - return await this._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} - */ - async findAllByCssSelector(selector) { - return this._wrapAll(await this._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} - */ - async findByCssSelector(selector) { - return this._wrap(await this._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} - */ - async isEnabled() { - return await this._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} - */ - async findAllByTagName(tagName) { - return this._wrapAll(await this._leadfootElement.findAllByTagName(tagName)); - } - - /** - * Gets a property of the element. - * https://theintern.io/leadfoot/module-leadfoot_Element.html#getProperty - * - * @param {string} name - * @return {Promise} - */ - async getProperty(name) { - return await this._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 - * - * @param {number} xOffset optional - The x-offset of the cursor, maybe in CSS pixels, relative to the left edge of the specified element’s bounding client rectangle. - * @param {number} yOffset optional - The y-offset of the cursor, maybe in CSS pixels, relative to the top edge of the specified element’s bounding client rectangle. - * @return {Promise} - */ - async moveMouseTo(xOffset, yOffset) { - await this.scrollIntoViewIfNecessary(); - return await this._leadfoot.moveMouseTo(this._leadfootElement, xOffset, yOffset); - } - - /** - * Gets a CSS computed property value for the element. - * https://theintern.io/leadfoot/module-leadfoot_Element.html#getComputedStyle - * - * @param {string} propertyName - * @return {Promise} - */ - async getComputedStyle(propertyName) { - return await this._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}>} - */ - async getSize() { - return await this._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} - */ - async findByTagName(tagName) { - return this._wrap(await this._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} - */ - async isSelected() { - return await this._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} - */ - async findDisplayedByCssSelector(selector) { - return this._wrap(await this._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} - */ - async waitForDeletedByClassName(className) { - await this._leadfootElement.waitForDeletedByClassName(className); - } - - /** - * Gets the first element inside this element partially matching the given case-insensitive link text. - * https://theintern.io/leadfoot/module-leadfoot_Element.html#findByPartialLinkText - * - * @param {string} text - * @return {Promise} - */ - async findByPartialLinkText(text) { - return this._wrap(await this._leadfootElement.findByPartialLinkText(text)); - } - - /** - * https://theintern.io/leadfoot/module-leadfoot_Element.html#findByXpath - * - * @deprecated - * @param {string} xpath - * @return {Promise} - */ - async findByXpath(xpath) { - return this._wrap(await this._leadfootElement.findByXpath(xpath)); - } - - /** - * Sends key event into element. - * https://theintern.io/leadfoot/module-leadfoot_Session.html#pressKeys - * - * @param {string|string[]} keys - * @return {Promise} - */ - async pressKeys(...args) { - await this._leadfoot.pressKeys(...args); - } - - /** - * Scroll the element into view, avoiding the fixed header if necessary - * - * @nonstandard - * @return {Promise} - */ - async scrollIntoViewIfNecessary() { - await this._leadfoot.execute(scrollIntoViewIfNecessary, [this._leadfootElement, this._fixedHeaderHeight]); - } - - /** - * Gets element innerHTML and wrap it up with cheerio - * - * @nonstandard - * @return {Promise} - */ - async parseDomContent() { - const htmlContent = await this.getProperty('innerHTML'); - const $ = cheerio.load(htmlContent, { - normalizeWhitespace: true, - xmlMode: true - }); - - $.findTestSubjects = function testSubjects(selector) { - return this(testSubjSelector(selector)); - }; - - $.fn.findTestSubjects = function testSubjects(selector) { - return this.find(testSubjSelector(selector)); - }; - - $.findTestSubject = $.fn.findTestSubject = function testSubjects(selector) { - return this.findTestSubjects(selector).first(); - }; - - return $; - } -} diff --git a/test/functional/services/remote/browser_driver_api/index.js b/test/functional/services/lib/web_element_wrapper/index.js similarity index 93% rename from test/functional/services/remote/browser_driver_api/index.js rename to test/functional/services/lib/web_element_wrapper/index.js index acc228e17086f..72f83a6a303a9 100644 --- a/test/functional/services/remote/browser_driver_api/index.js +++ b/test/functional/services/lib/web_element_wrapper/index.js @@ -17,4 +17,4 @@ * under the License. */ -export { BrowserDriverApi } from './browser_driver_api'; +export { WebElementWrapper } from './web_element_wrapper'; diff --git a/test/functional/services/lib/leadfoot_element_wrapper/scroll_into_view_if_necessary.js b/test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js similarity index 100% rename from test/functional/services/lib/leadfoot_element_wrapper/scroll_into_view_if_necessary.js rename to test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.js b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.js new file mode 100644 index 0000000000000..968dc8622fe5c --- /dev/null +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.js @@ -0,0 +1,446 @@ +/* + * 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 { scrollIntoViewIfNecessary } from './scroll_into_view_if_necessary'; +import { delay } from 'bluebird'; +import cheerio from 'cheerio'; +import testSubjSelector from '@kbn/test-subj-selector'; + +export class WebElementWrapper { + constructor(webElement, webDriver, timeout, fixedHeaderHeight, log) { + if (webElement instanceof WebElementWrapper) { + return webElement; + } + + this._webElement = webElement; + this._webDriver = webDriver; + this._driver = webDriver.driver; + this._By = webDriver.By; + this._Keys = webDriver.Key; + this._LegacyAction = webDriver.LegacyActionSequence; + this._defaultFindTimeout = timeout; + this._fixedHeaderHeight = fixedHeaderHeight; + this._logger = log; + } + + _wrap(otherWebElement) { + return new WebElementWrapper(otherWebElement, this._webDriver, this._defaultFindTimeout, this._fixedHeaderHeight, this._logger); + } + + _wrapAll(otherWebElements) { + return otherWebElements.map(e => this._wrap(e)); + } + + /** + * 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://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#isDisplayed + * + * @return {Promise} + */ + async isDisplayed() { + return await this._webElement.isDisplayed(); + } + + /** + * Tests whether this element is enabled, as dictated by the disabled attribute. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#isEnabled + * + * @return {Promise} + */ + async isEnabled() { + return await this._webElement.isEnabled(); + } + + /** + * Tests whether this element is selected. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#isSelected + * + * @return {Promise} + */ + async isSelected() { + return await this._webElement.isSelected(); + } + + /** + * Clicks on this element. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#click + * + * @return {Promise} + */ + async click() { + await this.scrollIntoViewIfNecessary(); + await this._webElement.click(); + } + + /** + * Clear the value of this element. This command has no effect if the underlying DOM element + * is neither a text INPUT element nor a TEXTAREA element. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#clear + * + * @return {Promise} + */ + async clearValue() { + await this._webElement.clear(); + } + + /** + * Types a key sequence on the DOM element represented by this instance. Modifier keys + * (SHIFT, CONTROL, ALT, META) are stateful; once a modifier is processed in the key sequence, + * that key state is toggled until one of the following occurs: + * + * The modifier key is encountered again in the sequence. At this point the state of the key is + * toggled (along with the appropriate keyup/down events). + * The input.Key.NULL key is encountered in the sequence. When this key is encountered, all + * modifier keys current in the down state are released (with accompanying keyup events). The NULL + * key can be used to simulate common keyboard shortcuts. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#sendKeys + * + * @param {string|string[]} value + * @param {charByChar: boolean} options + * @return {Promise} + */ + async type(value, options = { charByChar: false }) { + if (options.charByChar) { + for (const char of value) { + await this._webElement.sendKeys(char); + await delay(100); + } + } else { + await this._webElement.sendKeys(...value); + } + } + + /** + * Sends keyboard event into the element. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#sendKeys + * + * @param {string|string[]} keys + * @return {Promise} + */ + async pressKeys(...args) { + let chord; + //leadfoot compatibility + if (Array.isArray(args[0])) { + chord = this._Keys.chord(...args[0]); + } else { + chord = this._Keys.chord(...args); + } + + await this._webElement.sendKeys(chord); + } + + /** + * Retrieves the current value of the given attribute of this element. Will return the current + * value, even if it has been modified after the page has been loaded. More exactly, this method + * will return the value of the given attribute, unless that attribute is not present, in which + * case the value of the property with the same name is returned. If neither value is set, null + * is returned (for example, the "value" property of a textarea element). The "style" attribute + * is converted as best can be to a text representation with a trailing semi-colon. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#getAttribute + * + * @param {string} name + */ + async getAttribute(name) { + const rectAttributes = ['height', 'width', 'x', 'y']; + if (rectAttributes.includes(name)) { + const rect = await this.getSize(); + return rect[name]; + } + return await this._webElement.getAttribute(name); + } + + /** + * Retrieves the current value of the given attribute of this element. Will return the current + * value, even if it has been modified after the page has been loaded. More exactly, this method + * will return the value of the given attribute, unless that attribute is not present, in which + * case the value of the property with the same name is returned. If neither value is set, null + * is returned (for example, the "value" property of a textarea element). The "style" attribute + * is converted as best can be to a text representation with a trailing semi-colon. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#getAttribute + * + * @param {string} name + * @return {Promise} + */ + async getProperty(name) { + + const property = await this._webElement.getAttribute(name); + + // leadfoot compatibility convertion + if (property == null) { + return false; + } + if (['true', 'false'].includes(property)) { + return property === 'true'; + } else { + return property; + } + } + + /** + * Retrieves the value of a computed style property for this instance. If the element inherits + * the named style from its parent, the parent will be queried for its value. Where possible, + * color values will be converted to their hex representation (e.g. #00ff00 instead of rgb(0, 255, 0)). + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#getCssValue + * + * @param {string} propertyName + * @return {Promise} + */ + async getComputedStyle(propertyName) { + return await this._webElement.getCssValue(propertyName); + } + + /** + * Get the visible (i.e. not hidden by CSS) innerText of this element, including sub-elements, + * without any leading or trailing whitespace. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#getText + * + * @return {Promise} + */ + async getVisibleText() { + return await this._webElement.getText(); + } + + /** + * Retrieves the element's tag name. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#getTagName + * + * @return {Promise} + */ + async getTagName() { + return await this._webElement.getTagName(); + } + + /** + * Returns an object describing an element's location, in pixels relative to the document element, + * and the element's size in pixels. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#getRect + * + * @return {Promise<{height: number, width: number, x: number, y: number}>} + */ + async getPosition() { + return await this._webElement.getRect(); + } + + /** + * Returns an object describing an element's location, in pixels relative to the document element, + * and the element's size in pixels. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#getRect + * + * @return {Promise<{height: number, width: number, x: number, y: number}>} + */ + async getSize() { + return await this._webElement.getRect(); + } + + /** + * Moves the remote environment’s mouse cursor to the current element + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move + * + * @return {Promise} + */ + async moveMouseTo() { + await this.scrollIntoViewIfNecessary(); + const mouse = this._driver.actions().mouse(); + const actions = this._driver.actions({ bridge: true }); + await actions.pause(mouse).move({ origin: this._webElement }).perform(); + } + + /** + * Gets the first element inside this element matching the given CSS selector. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} selector + * @return {Promise} + */ + async findByCssSelector(selector) { + return this._wrap(await this._webElement.findElement(this._By.css(selector))); + } + + /** + * Gets all elements inside this element matching the given CSS selector. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} selector + * @return {Promise} + */ + async findAllByCssSelector(selector) { + return this._wrapAll(await this._webElement.findElements(this._By.css(selector))); + } + + /** + * Gets the first element inside this element matching the given CSS class name. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} className + * @return {Promise} + */ + async findByClassName(className) { + return this._wrap(await this._webElement.findElement(this._By.className(className))); + } + + /** + * Gets all elements inside this element matching the given CSS class name. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} className + * @return {Promise} + */ + async findAllByClassName(className) { + return await this._wrapAll( + await this._webElement.findElements(this._By.className(className)) + ); + } + + /** + * Gets the first element inside this element matching the given tag name. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} tagName + * @return {Promise} + */ + async findByTagName(tagName) { + return this._wrap(await this._webElement.findElement(this._By.tagName(tagName))); + } + + /** + * Gets all elements inside this element matching the given tag name. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} tagName + * @return {Promise} + */ + async findAllByTagName(tagName) { + return await this._wrapAll( + await this._webElement.findElements(this._By.tagName(tagName)) + ); + } + + /** + * Gets the first element inside this element matching the given XPath selector. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} selector + * @return {Promise} + */ + async findByXpath(selector) { + return this._wrap(await this._webElement.findElement(this._By.xpath(selector))); + } + + /** + * Gets all elements inside this element matching the given XPath selector. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} selector + * @return {Promise} + */ + async findAllByXpath(selector) { + return await this._wrapAll( + await this._webElement.findElements(this._By.xpath(selector)) + ); + } + + /** + * Gets the first element inside this element matching the given partial link text. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} selector + * @return {Promise} + */ + async findByPartialLinkText(linkText) { + return await this._wrap(await this._webElement.findElement(this._By.partialLinkText(linkText))); + } + + /** + * Gets all elements inside this element matching the given partial link text. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement + * + * @param {string} selector + * @return {Promise} + */ + async findAllByPartialLinkText(linkText) { + return await this._wrapAll( + await this._webElement.findElements(this._By.partialLinkText(linkText)) + ); + } + + /** + * Waits for all elements inside this element matching the given CSS class name to be destroyed. + * + * @param {string} className + * @return {Promise} + */ + async waitForDeletedByClassName(className) { + await this._driver.wait(() => { + return this._webElement.findElements(this._By.className(className)).then((children) => { + if (children.length <= 0) { + return true; + } + return false; + }); + }, + this._defaultFindTimeout, + `The element with ${className} className was still present when it should have disappeared.`); + } + + /** + * Scroll the element into view, avoiding the fixed header if necessary + * + * @nonstandard + * @return {Promise} + */ + async scrollIntoViewIfNecessary() { + await this._driver.executeScript(scrollIntoViewIfNecessary, this._webElement, this._fixedHeaderHeight); + } + + /** + * Gets element innerHTML and wrap it up with cheerio + * + * @nonstandard + * @return {Promise} + */ + async parseDomContent() { + const htmlContent = await this.getProperty('innerHTML'); + const $ = cheerio.load(htmlContent, { + normalizeWhitespace: true, + xmlMode: true + }); + + $.findTestSubjects = function testSubjects(selector) { + return this(testSubjSelector(selector)); + }; + + $.fn.findTestSubjects = function testSubjects(selector) { + return this.find(testSubjSelector(selector)); + }; + + $.findTestSubject = $.fn.findTestSubject = function testSubjects(selector) { + return this.findTestSubjects(selector).first(); + }; + + return $; + } +} diff --git a/test/functional/services/remote/browser_driver_api/browser_driver_api.js b/test/functional/services/remote/browser_driver_api/browser_driver_api.js deleted file mode 100644 index 57d7bd93b3629..0000000000000 --- a/test/functional/services/remote/browser_driver_api/browser_driver_api.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 { EventEmitter } from 'events'; - -import { createLocalBrowserDriverApi } from './browser_driver_local_api'; -import { createRemoteBrowserDriverApi } from './browser_driver_remote_api'; -import { ping } from './ping'; - -const noop = () => {}; - -/** - * Api for interacting with a local or remote instance of a browser - * - * @type {Object} - */ -export class BrowserDriverApi extends EventEmitter { - static async factory(log, url, browserType) { - return (await ping(url)) - ? createRemoteBrowserDriverApi(log, url) - : createLocalBrowserDriverApi(log, url, browserType); - } - - constructor(options = {}) { - super(); - - const { - url, - start = noop, - stop = noop, - requiredCapabilities, - } = options; - - if (!url) { - throw new TypeError('url is a required parameter'); - } - this._requiredCapabilities = requiredCapabilities; - this._url = url; - this._state = undefined; - this._callCustomStart = () => start(this); - this._callCustomStop = () => stop(this); - this._beforeStopFns = []; - } - getRequiredCapabilities() { - return this._requiredCapabilities; - } - getUrl() { - return this._url; - } - - beforeStop(fn) { - this._beforeStopFns.push(fn); - } - - isStopped() { - return this._state === 'stopped'; - } - - async start() { - if (this._state !== undefined) { - throw new Error('Driver can only be started once'); - } - - this._state = 'started'; - await this._callCustomStart(); - } - - async stop() { - if (this._state !== 'started') { - throw new Error('Driver can only be stopped after being started'); - } - - this._state = 'stopped'; - - for (const fn of this._beforeStopFns.splice(0)) { - await fn(); - } - - await this._callCustomStop(); - } -} diff --git a/test/functional/services/remote/browser_driver_api/browser_driver_local_api.js b/test/functional/services/remote/browser_driver_api/browser_driver_local_api.js deleted file mode 100644 index f51dfdaa1a883..0000000000000 --- a/test/functional/services/remote/browser_driver_api/browser_driver_local_api.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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 { spawn } from 'child_process'; -import { parse as parseUrl } from 'url'; - -import treeKill from 'tree-kill'; -import { delay, fromNode as fcb } from 'bluebird'; -import { path as CHROMEDRIVER_EXEC } from 'chromedriver'; -import { path as FIREFOXDRIVER_EXEC } from 'geckodriver'; - -import { ping } from './ping'; -import { BrowserDriverApi } from './browser_driver_api'; -const START_TIMEOUT = 15000; -const PING_INTERVAL = 500; - -export function createLocalBrowserDriverApi(log, url, browser) { - let runningDriver = null; - const driverName = browser + 'driver'; - switch (browser) { - case 'firefox': - runningDriver = FIREFOXDRIVER_EXEC; - break; - default: - runningDriver = CHROMEDRIVER_EXEC; - } - let proc = null; - - return new BrowserDriverApi({ - url, - requiredCapabilities: Object.create({ browserType: browser }), - - async start(api) { - const { port } = parseUrl(url); - log.debug('Starting local ' + driverName + ' at port %d', port); - - proc = spawn(runningDriver, [`--port=${port}`], { - stdio: ['ignore', 'pipe', 'pipe'], - }); - - proc.stdout.on('data', chunk => { - log.debug('[' + driverName + ':stdout]', chunk.toString('utf8').trim()); - }); - proc.stderr.on('data', chunk => { - log.debug('[' + driverName + ':stderr]', chunk.toString('utf8').trim()); - }); - - proc.on('exit', (code) => { - if (!api.isStopped() && code > 0) { - api.emit('error', new Error(driverName + ` exited with code ${code}`)); - } - }); - - const pingsStartedAt = Date.now(); - while (true) { - log.debug('[' + driverName + ':ping] attempting to reach at %j', url); - if (await ping(url)) { - log.debug('[' + driverName + ':ping] success'); - break; - } else { - log.debug('[' + driverName + ':ping] failure'); - } - - if ((Date.now() - pingsStartedAt) < START_TIMEOUT) { - log.debug('[' + driverName + ':ping] waiting for %d before next ping', PING_INTERVAL); - await delay(PING_INTERVAL); - continue; - } - - throw new Error(driverName + ` did not start within the ${START_TIMEOUT}ms timeout`); - } - }, - - async stop() { - await fcb(cb => treeKill(proc.pid, undefined, cb)); - } - }); -} diff --git a/test/functional/services/remote/browser_driver_api/ping.js b/test/functional/services/remote/browser_driver_api/ping.js deleted file mode 100644 index 65b62a8b7b4dc..0000000000000 --- a/test/functional/services/remote/browser_driver_api/ping.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 request from 'request'; -import { fromNode as fcb } from 'bluebird'; - -export async function ping(url) { - try { - await Promise.race([ - fcb(cb => request(url, cb)), - new Promise((resolve, reject) => { - setTimeout(() => reject(new Error('timeout')), 1000); - }) - ]); - return true; - } catch (err) { - return false; - } -} diff --git a/test/functional/services/remote/leadfoot_command.js b/test/functional/services/remote/leadfoot_command.js deleted file mode 100644 index c0b5108040996..0000000000000 --- a/test/functional/services/remote/leadfoot_command.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 { delay } from 'bluebird'; -import Command from 'leadfoot/Command'; -import Server from 'leadfoot/Server'; - -import { initVerboseRemoteLogging } from './verbose_remote_logging'; - -const SECOND = 1000; -const MINUTE = 60 * SECOND; - -let attemptCounter = 0; -async function attemptToCreateCommand(log, server, driverApi) { - const attemptId = ++attemptCounter; - log.debug('[leadfoot:command] Creating session'); - - let browserOptions = {}; - if (process.env.TEST_DISABLE_GPU) { - browserOptions = { chromeOptions: { args: ['disable-gpu'] } }; - } - if (process.env.TEST_BROWSER_HEADLESS) { - browserOptions = { chromeOptions: { args: ['headless', 'disable-gpu'] } }; - } - const session = await server.createSession(browserOptions, driverApi.getRequiredCapabilities()); - - if (attemptId !== attemptCounter) return; // abort - - log.debug('[leadfoot:command] Registering session for teardown'); - driverApi.beforeStop(async () => session.quit()); - if (attemptId !== attemptCounter) return; // abort - - log.debug('[leadfoot:command] Completing session capabilities'); - await server._fillCapabilities(session); - if (attemptId !== attemptCounter) return; // abort - - // command looks like a promise because it has a `.then()` function - // so we wrap it in an object to prevent async/await from trying to - // unwrap/resolve it - return { command: new Command(session) }; -} - -export async function initLeadfootCommand({ log, browserDriverApi }) { - return await Promise.race([ - (async () => { - await delay(6 * MINUTE); - throw new Error('remote failed to start within 6 minutes'); - })(), - - (async () => { - // a `leadfoot/Server` object knows how to communicate with the webdriver - // backend (chromedriver in this case). it helps with session management - // and all communication to the remote browser go through it, so we shim - // some of it's methods to enable very verbose logging. - const server = initVerboseRemoteLogging(log, new Server(browserDriverApi.getUrl())); - - // by default, calling server.createSession() automatically fixes the webdriver - // "capabilities" hash so that leadfoot knows the hoops it has to jump through - // to have feature compliance. This is sort of like building "$.support" in jQuery. - // Unfortunately this process takes a couple seconds, so if we let leadfoot - // do it and we have an error, are killed, or for any other reason have to - // teardown we won't have a session object until the auto-fixing is complete. - // - // To avoid this we disable auto-fixing with this flag and call - // `server._fillCapabilities()` ourselves to do the fixing once we have a reference - // to the session and have registered it for teardown before stopping the - // chromedriverApi. - server.fixSessionCapabilities = false; - - while (true) { - const command = await Promise.race([ - delay(6 * MINUTE), - attemptToCreateCommand(log, server, browserDriverApi) - ]); - - if (!command) { - continue; - } - - return command; - } - })() - ]); -} diff --git a/test/functional/services/remote/browser_driver_api/browser_driver_remote_api.js b/test/functional/services/remote/prevent_parallel_calls.js similarity index 52% rename from test/functional/services/remote/browser_driver_api/browser_driver_remote_api.js rename to test/functional/services/remote/prevent_parallel_calls.js index 3b8e486c2e55f..511c205093ea3 100644 --- a/test/functional/services/remote/browser_driver_api/browser_driver_remote_api.js +++ b/test/functional/services/remote/prevent_parallel_calls.js @@ -17,15 +17,39 @@ * under the License. */ -import { BrowserDriverApi } from './browser_driver_api'; +export function preventParallelCalls(fn, filter) { + const execQueue = []; -export function createRemoteBrowserDriverApi(log, url) { - return new BrowserDriverApi({ - url, + return async function (arg) { + if (filter(arg)) { + return await fn.call(this, arg); + } + + const task = { + exec: async () => { + try { + task.resolve(await fn.call(this, arg)); + } catch (error) { + task.reject(error); + } finally { + execQueue.shift(); + if (execQueue.length) { + execQueue[0].exec(); + } + } + } + }; + + task.promise = new Promise((resolve, reject) => { + task.resolve = resolve; + task.reject = reject; + }); - start() { - log.info(`Reusing instance at %j`, url); + if (execQueue.push(task) === 1) { + // only item in the queue, kick it off + task.exec(); } - }); + return task.promise; + }; } diff --git a/test/functional/services/remote/prevent_parallel_calls.test.js b/test/functional/services/remote/prevent_parallel_calls.test.js new file mode 100644 index 0000000000000..03cf4c671be53 --- /dev/null +++ b/test/functional/services/remote/prevent_parallel_calls.test.js @@ -0,0 +1,55 @@ +/* + * 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 { preventParallelCalls } from './prevent_parallel_calls'; + +it('only calls fn when previous call is complete, ignores when filter returns true', async () => { + const orderOfEvents = []; + + async function foo(arg) { + orderOfEvents.push(`called with ${arg}`); + await new Promise(resolve => setTimeout(resolve, arg)); + orderOfEvents.push(`resolved with ${arg}`); + } + + const serialized = preventParallelCalls(foo, arg => arg === 0); + + await Promise.all([ + serialized(100), + serialized(0), + serialized(150), + serialized(170), + serialized(50), + ]); + + expect(orderOfEvents).toMatchInlineSnapshot(` +Array [ + "called with 100", + "called with 0", + "resolved with 0", + "resolved with 100", + "called with 150", + "resolved with 150", + "called with 170", + "resolved with 170", + "called with 50", + "resolved with 50", +] +`); +}); diff --git a/test/functional/services/remote/remote.js b/test/functional/services/remote/remote.js index 7fa95b685d419..cea5f2b69d3a9 100644 --- a/test/functional/services/remote/remote.js +++ b/test/functional/services/remote/remote.js @@ -17,54 +17,44 @@ * under the License. */ -import { initLeadfootCommand } from './leadfoot_command'; -import { BrowserDriverApi } from './browser_driver_api'; +import { initWebDriver } from './webdriver'; export async function RemoteProvider({ getService }) { const lifecycle = getService('lifecycle'); - const config = getService('config'); const log = getService('log'); - const possibleBrowsers = ['chrome', 'firefox']; + const config = getService('config'); + const possibleBrowsers = ['chrome', 'firefox', 'ie']; const browserType = process.env.TEST_BROWSER_TYPE || 'chrome'; if (!possibleBrowsers.includes(browserType)) { throw new Error(`Unexpected TEST_BROWSER_TYPE "${browserType}". Valid options are ` + possibleBrowsers.join(',')); } - const browserDriverApi = await BrowserDriverApi.factory(log, config.get(browserType + 'driver.url'), browserType); - lifecycle.on('cleanup', async () => await browserDriverApi.stop()); - - await browserDriverApi.start(); - - const { command } = await initLeadfootCommand({ log, browserDriverApi: browserDriverApi }); + const { driver, By, Key, until, LegacyActionSequence } = await initWebDriver({ log, browserType }); log.info('Remote initialized'); lifecycle.on('beforeTests', async () => { // hard coded default, can be overridden per suite using `browser.setWindowSize()` // and will be automatically reverted after each suite - await command.setWindowSize(1600, 1000); + await driver.manage().window().setRect({ width: 1600, height: 1000 }); }); const windowSizeStack = []; lifecycle.on('beforeTestSuite', async () => { - windowSizeStack.unshift(await command.getWindowSize()); + windowSizeStack.unshift(await driver.manage().window().getRect()); + }); + + lifecycle.on('beforeEachTest', async () => { + await driver.manage().setTimeouts({ implicit: config.get('timeouts.find') }); }); lifecycle.on('afterTestSuite', async () => { const { width, height } = windowSizeStack.shift(); - await command.setWindowSize(width, height); + await driver.manage().window().setRect({ width: width, height: height }); }); - return new Proxy({}, { - get(obj, prop) { - if (prop === 'then' || prop === 'catch' || prop === 'finally') { - // prevent the remote from being treated like a promise by - // hiding it's promise-like properties - return undefined; - } + lifecycle.on('cleanup', async () => await driver.quit()); - return command[prop]; - } - }); + return { driver, By, Key, until, LegacyActionSequence }; } diff --git a/test/functional/services/remote/webdriver.js b/test/functional/services/remote/webdriver.js new file mode 100644 index 0000000000000..725d98bfbb25f --- /dev/null +++ b/test/functional/services/remote/webdriver.js @@ -0,0 +1,136 @@ +/* + * 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 { delay } from 'bluebird'; +import { Builder, By, Key, until, logging } from 'selenium-webdriver'; +const { LegacyActionSequence } = require('selenium-webdriver/lib/actions'); +const { getLogger } = require('selenium-webdriver/lib/logging'); +const { Executor } = require('selenium-webdriver/lib/http'); +const chrome = require('selenium-webdriver/chrome'); +const firefox = require('selenium-webdriver/firefox'); +const geckoDriver = require('geckodriver'); +const chromeDriver = require('chromedriver'); +const throttleOption = process.env.TEST_THROTTLE_NETWORK; + +import { preventParallelCalls } from './prevent_parallel_calls'; + +const SECOND = 1000; +const MINUTE = 60 * SECOND; +const NO_QUEUE_COMMANDS = [ + 'getStatus', + 'newSession', + 'quit' +]; + +/** + * Best we can tell WebDriver locks up sometimes when we send too many + * commands at once, sometimes... It causes random lockups where we never + * receive another response from WedDriver and we don't want to live with + * that risk, so for now I've shimmed the Executor class in WebDiver to + * queue all calls to Executor#send() if there is already a call in + * progress. + */ +Executor.prototype.execute = preventParallelCalls(Executor.prototype.execute, (command) => ( + NO_QUEUE_COMMANDS.includes(command.getName()) +)); + +let attemptCounter = 0; +async function attemptToCreateCommand(log, browserType) { + const attemptId = ++attemptCounter; + log.debug('[webdriver] Creating session'); + + const buildDriverInstance = async (browserType) => { + switch (browserType) { + case 'chrome': + const chromeOptions = new chrome.Options(); + const loggingPref = new logging.Preferences().setLevel(logging.Type.BROWSER, logging.Level.ALL); + chromeOptions.setLoggingPrefs(loggingPref); + if (process.env.TEST_BROWSER_HEADLESS) { + //Use --disable-gpu to avoid an error from a missing Mesa library, as per + //See: https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md + chromeOptions.addArguments('headless', 'disable-gpu'); + } + return new Builder() + .forBrowser(browserType) + .setChromeOptions(chromeOptions) + .setChromeService(new chrome.ServiceBuilder(chromeDriver.path).enableVerboseLogging()) + .build(); + case 'firefox': + const firefoxOptions = new firefox.Options(); + if (process.env.TEST_BROWSER_HEADLESS) { + //See: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode + firefoxOptions.addArguments('-headless'); + } + return new Builder() + .forBrowser(browserType) + .setFirefoxOptions(firefoxOptions) + .setFirefoxService(new firefox.ServiceBuilder(geckoDriver.path).enableVerboseLogging()) + .build(); + default: + throw new Error(`${browserType} is not supported yet`); + } + }; + + const session = await buildDriverInstance(browserType); + + if (throttleOption === 'true' && browserType === 'chrome') { //Only chrome supports this option. + log.debug('NETWORK THROTTLED: 768k down, 256k up, 100ms latency.'); + session.setNetworkConditions({ + offline: false, + latency: 100, // Additional latency (ms). + download_throughput: 768 * 1024, // These speeds are in bites per second, not kilobytes. + upload_throughput: 256 * 1024 + }); + } + + if (attemptId !== attemptCounter) return; // abort + + return { driver: session, By, Key, until, LegacyActionSequence }; +} + +export async function initWebDriver({ log, browserType }) { + const logger = getLogger('webdriver.http.Executor'); + logger.setLevel(logging.Level.FINEST); + logger.addHandler((entry) => { + log.verbose(entry.message); + }); + + + return await Promise.race([ + (async () => { + await delay(2 * MINUTE); + throw new Error('remote failed to start within 2 minutes'); + })(), + + (async () => { + while (true) { + const command = await Promise.race([ + delay(30 * SECOND), + attemptToCreateCommand(log, browserType) + ]); + + if (!command) { + continue; + } + + return command; + } + })() + ]); +} diff --git a/test/functional/services/screenshots.js b/test/functional/services/screenshots.js index da29b4072436c..99132796b5c2f 100644 --- a/test/functional/services/screenshots.js +++ b/test/functional/services/screenshots.js @@ -78,7 +78,7 @@ export async function ScreenshotsProvider({ getService }) { browser.takeScreenshot(), fcb(cb => mkdirp(dirname(path), cb)), ]); - await fcb(cb => writeFile(path, screenshot, cb)); + await fcb(cb => writeFile(path, screenshot, 'base64', cb)); } catch (err) { log.error('SCREENSHOT FAILED'); log.error(err); diff --git a/test/functional/services/test_subjects.js b/test/functional/services/test_subjects.js index 7efc733a89577..b29b570cd21c3 100644 --- a/test/functional/services/test_subjects.js +++ b/test/functional/services/test_subjects.js @@ -19,7 +19,6 @@ import testSubjSelector from '@kbn/test-subj-selector'; import { - filter as filterAsync, map as mapAsync, } from 'bluebird'; @@ -64,6 +63,7 @@ export function TestSubjectsProvider({ getService }) { async append(selector, text) { return await retry.try(async () => { + log.debug(`TestSubjects.append(${selector}, ${text})`); const input = await this.find(selector); await input.click(); await input.type(text); @@ -71,7 +71,7 @@ export function TestSubjectsProvider({ getService }) { } async clickWhenNotDisabled(selector, { timeout = FIND_TIME } = {}) { - log.debug(`TestSubjects.click(${selector})`); + log.debug(`TestSubjects.clickWhenNotDisabled(${selector})`); await find.clickByCssSelectorWhenNotDisabled(testSubjSelector(selector), { timeout }); } @@ -81,23 +81,26 @@ export function TestSubjectsProvider({ getService }) { } async doubleClick(selector, timeout = FIND_TIME) { - log.debug(`TestSubjects.doubleClick(${selector})`); return await retry.try(async () => { + log.debug(`TestSubjects.doubleClick(${selector})`); const element = await this.find(selector, timeout); - await browser.moveMouseTo(element); + await element.moveMouseTo(); await browser.doubleClick(); }); } async descendantExists(selector, parentElement) { + log.debug(`TestSubjects.descendantExists(${selector})`); return await find.descendantExistsByCssSelector(testSubjSelector(selector), parentElement); } async findDescendant(selector, parentElement) { + log.debug(`TestSubjects.findDescendant(${selector})`); return await find.descendantDisplayedByCssSelector(testSubjSelector(selector), parentElement); } async findAllDescendant(selector, parentElement) { + log.debug(`TestSubjects.findAllDescendant(${selector})`); return await find.allDescendantDisplayedByCssSelector(testSubjSelector(selector), parentElement); } @@ -107,18 +110,22 @@ export function TestSubjectsProvider({ getService }) { } async findAll(selector, timeout) { - log.debug(`TestSubjects.findAll(${selector})`); - const all = await find.allByCssSelector(testSubjSelector(selector), timeout); - return await filterAsync(all, el => el.isDisplayed()); + return await retry.try(async () => { + log.debug(`TestSubjects.findAll(${selector})`); + const all = await find.allByCssSelector(testSubjSelector(selector), timeout); + return await find.filterElementIsDisplayed(all); + }); } async getPropertyAll(selector, property) { + log.debug(`TestSubjects.getPropertyAll(${selector}, ${property})`); return await this._mapAll(selector, async (element) => { return await element.getProperty(property); }); } async getProperty(selector, property) { + log.debug(`TestSubjects.getProperty(${selector}, ${property})`); return await retry.try(async () => { const element = await this.find(selector); return await element.getProperty(property); @@ -126,6 +133,7 @@ export function TestSubjectsProvider({ getService }) { } async getAttributeAll(selector, attribute) { + log.debug(`TestSubjects.getAttributeAll(${selector}, ${attribute})`); return await this._mapAll(selector, async (element) => { return await element.getAttribute(attribute); }); @@ -133,6 +141,7 @@ export function TestSubjectsProvider({ getService }) { async getAttribute(selector, attribute) { return await retry.try(async () => { + log.debug(`TestSubjects.getAttribute(${selector}, ${attribute})`); const element = await this.find(selector); return await element.getAttribute(attribute); }); @@ -140,6 +149,7 @@ export function TestSubjectsProvider({ getService }) { async setValue(selector, text) { return await retry.try(async () => { + log.debug(`TestSubjects.setValue(${selector}, ${text})`); await this.click(selector); // in case the input element is actually a child of the testSubject, we // call clearValue() and type() on the element that is focused after @@ -152,6 +162,7 @@ export function TestSubjectsProvider({ getService }) { async isEnabled(selector) { return await retry.try(async () => { + log.debug(`TestSubjects.isEnabled(${selector})`); const element = await this.find(selector); return await element.isEnabled(); }); @@ -159,6 +170,7 @@ export function TestSubjectsProvider({ getService }) { async isDisplayed(selector) { return await retry.try(async () => { + log.debug(`TestSubjects.isDisplayed(${selector})`); const element = await this.find(selector); return await element.isDisplayed(); }); @@ -166,12 +178,14 @@ export function TestSubjectsProvider({ getService }) { async isSelected(selector) { return await retry.try(async () => { + log.debug(`TestSubjects.isSelected(${selector})`); const element = await this.find(selector); return await element.isSelected(); }); } async isSelectedAll(selectorAll) { + log.debug(`TestSubjects.isSelectedAll(${selectorAll})`); return await this._mapAll(selectorAll, async (element) => { return await element.isSelected(); }); @@ -179,12 +193,14 @@ export function TestSubjectsProvider({ getService }) { async getVisibleText(selector) { return await retry.try(async () => { + log.debug(`TestSubjects.getVisibleText(${selector})`); const element = await this.find(selector); return await element.getVisibleText(); }); } async getVisibleTextAll(selectorAll) { + log.debug(`TestSubjects.getVisibleTextAll(${selectorAll})`); return await this._mapAll(selectorAll, async (element) => { return await element.getVisibleText(); }); @@ -195,8 +211,9 @@ export function TestSubjectsProvider({ getService }) { // have run into a case where the element becomes stale after the find succeeds, throwing an error during the // moveMouseTo function. await retry.try(async () => { + log.debug(`TestSubjects.moveMouseTo(${selector})`); const element = await this.find(selector); - await browser.moveMouseTo(element); + await element.moveMouseTo(); }); } @@ -207,8 +224,12 @@ export function TestSubjectsProvider({ getService }) { }); } - async waitForDeleted(selector) { - await find.waitForDeletedByCssSelector(testSubjSelector(selector)); + async waitForDeleted(selectorOrElement) { + if (typeof (selectorOrElement) === 'string') { + await find.waitForDeletedByCssSelector(testSubjSelector(selectorOrElement)); + } else { + await find.waitForElementStale(selectorOrElement); + } } async waitForAttributeToChange(selector, attribute, value) { diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index 73f80591c2a79..7eb71a1196b76 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -20,7 +20,7 @@ export function InfraHomePageProvider({ getService }: KibanaFunctionalTestDefaul `${testSubjSelector('waffleDatePicker')} .euiDatePicker.euiFieldText` ); - await datePickerInput.type(Array(30).fill(browser.keys.BACKSPACE)); + await datePickerInput.type(Array(30).fill(browser.keys.BACK_SPACE)); await datePickerInput.type([moment(time).format('L LTS'), browser.keys.RETURN]); }, diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.js index fee1b47826e59..8dae2d5e3b524 100644 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ b/x-pack/test/functional/page_objects/reporting_page.js @@ -126,7 +126,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) { async clearToastNotifications() { const toasts = await testSubjects.findAll('toastCloseButton'); - await Promise.all(toasts.map(t => t.click())); + await Promise.all(toasts.map(async t => await t.click())); } async getQueueReportError() { @@ -134,7 +134,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) { } async getGenerateReportButton() { - return await retry.try(() => testSubjects.find('generateReportButton')); + return await retry.try(async () => await testSubjects.find('generateReportButton')); } async checkUsePrintLayout() { @@ -146,16 +146,20 @@ export function ReportingPageProvider({ getService, getPageObjects }) { } async clickGenerateReportButton() { - await retry.try(() => testSubjects.click('generateReportButton')); + await testSubjects.click('generateReportButton'); } async checkForReportingToasts() { log.debug('Reporting:checkForReportingToasts'); const isToastPresent = await testSubjects.exists('completeReportSuccess', { - timeout: 60000 + allowHidden: true, + timeout: 90000 }); // Close toast so it doesn't obscure the UI. - await testSubjects.click('completeReportSuccess toastCloseButton'); + if (isToastPresent) { + await testSubjects.click('completeReportSuccess toastCloseButton'); + } + return isToastPresent; } diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 1e3ccea64e218..33f064388aa6c 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -299,7 +299,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.clickBucket('X-Axis'); await PageObjects.visualize.selectAggregation('Date Histogram'); await PageObjects.visualize.clickGo(); - await PageObjects.visualize.saveVisualizationExpectSuccess('my viz'); + await PageObjects.visualize.saveVisualization('my viz'); await PageObjects.reporting.openPdfReportingPanel(); await expectEnabledGenerateReportButton(); }); diff --git a/x-pack/test/types/leadfoot.d.ts b/x-pack/test/types/leadfoot.d.ts deleted file mode 100644 index a9a081afa8114..0000000000000 --- a/x-pack/test/types/leadfoot.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -declare module 'leadfoot/keys' { - type LeadfootKeys = 'BACKSPACE' | 'ENTER' | 'RETURN'; - - const keys: { [key in LeadfootKeys]: string }; - export default keys; -} diff --git a/yarn.lock b/yarn.lock index 4b15ee549b481..92893b4c4e5c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6194,6 +6194,11 @@ core-js@^2.5.1, core-js@^2.5.3, core-js@^2.5.7: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== +core-js@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65" + integrity sha1-+rg/uwstjchfpjbEudNMdUIMbWU= + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -7378,11 +7383,6 @@ doctypes@^1.1.0: resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= -dojo@2.0.0-alpha.7: - version "2.0.0-alpha.7" - resolved "https://registry.yarnpkg.com/dojo/-/dojo-2.0.0-alpha.7.tgz#c2b25d43d8f72ccc9c8fe89a34906a2d271e5c91" - integrity sha1-wrJdQ9j3LMycj+iaNJBqLSceXJE= - "dom-helpers@^2.4.0 || ^3.0.0": version "3.3.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" @@ -7960,6 +7960,11 @@ es6-promise@^4.0.3, es6-promise@~4.2.4: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" integrity sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ== +es6-promise@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" + integrity sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y= + es6-promisify@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" @@ -11359,6 +11364,11 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -13270,12 +13280,16 @@ jsx-ast-utils@^2.0.1: dependencies: array-includes "^3.0.3" -jszip@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-2.5.0.tgz#7444fd8551ddf3e5da7198fea0c91bc8308cc274" - integrity sha1-dET9hVHd8+XacZj+oMkbyDCMwnQ= +jszip@^3.1.3: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.1.5.tgz#e3c2a6c6d706ac6e603314036d43cd40beefdf37" + integrity sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ== dependencies: - pako "~0.2.5" + core-js "~2.3.0" + es6-promise "~3.0.2" + lie "~3.1.0" + pako "~1.0.2" + readable-stream "~2.0.6" just-extend@^1.1.27: version "1.1.27" @@ -13530,14 +13544,6 @@ lead@^1.0.0: dependencies: flush-write-stream "^1.0.2" -leadfoot@1.7.5: - version "1.7.5" - resolved "https://registry.yarnpkg.com/leadfoot/-/leadfoot-1.7.5.tgz#2188019ba95f524f2fec4dd9fbb06f1e6e832d3e" - integrity sha512-NLPJyZ5HYjM2PbMzkpF89EwOboEpyE/ceyLNOmp7TzFZwJQDdxSxdEj3kuJtnfVCUSZXzxvNMUtO300bS+j4nA== - dependencies: - dojo "2.0.0-alpha.7" - jszip "2.5.0" - leaflet-draw@0.4.10: version "0.4.10" resolved "https://registry.yarnpkg.com/leaflet-draw/-/leaflet-draw-0.4.10.tgz#a611a29925a32cde63638e891c3bfc93163e2f43" @@ -13621,6 +13627,13 @@ license-checker@^16.0.0: spdx-satisfies "^0.1.3" treeify "^1.0.1" +lie@~3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4= + dependencies: + immediate "~3.0.5" + liftoff@^2.1.0: version "2.5.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.5.0.tgz#2009291bb31cea861bbf10a7c15a28caf75c31ec" @@ -16237,11 +16250,16 @@ pad-component@0.0.1: resolved "https://registry.yarnpkg.com/pad-component/-/pad-component-0.0.1.tgz#ad1f22ce1bf0fdc0d6ddd908af17f351a404b8ac" integrity sha1-rR8izhvw/cDW3dkIrxfzUaQEuKw= -pako@^0.2.5, pako@~0.2.5: +pako@^0.2.5: version "0.2.9" resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU= +pako@~1.0.2: + version "1.0.8" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.8.tgz#6844890aab9c635af868ad5fecc62e8acbba3ea4" + integrity sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA== + pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" @@ -18129,7 +18147,7 @@ readable-stream@~1.1.0, readable-stream@~1.1.9: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@~2.0.0: +readable-stream@~2.0.0, readable-stream@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" integrity sha1-j5A0HmilPMySh4jaz80Rs265t44= @@ -19238,6 +19256,16 @@ select@^1.1.2: resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= +selenium-webdriver@^4.0.0-alpha.1: + version "4.0.0-alpha.1" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.1.tgz#cc93415e21d2dc1dfd85dfc5f6b55f3ac53933b1" + integrity sha512-z88rdjHAv3jmTZ7KSGUkTvo4rGzcDGMq0oXWHNIDK96Gs31JKVdu9+FMtT4KBrVoibg8dUicJDok6GnqqttO5Q== + dependencies: + jszip "^3.1.3" + rimraf "^2.5.4" + tmp "0.0.30" + xml2js "^0.4.17" + selfsigned@^1.9.1: version "1.10.2" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.2.tgz#b4449580d99929b65b10a48389301a6592088758" @@ -20980,6 +21008,13 @@ tmp@0.0.23: resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.23.tgz#de874aa5e974a85f0a32cdfdbd74663cb3bd9c74" integrity sha1-3odKpel0qF8KMs39vXRmPLO9nHQ= +tmp@0.0.30: + version "0.0.30" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" + integrity sha1-ckGdSovn1s51FI/YsyTlk6cRwu0= + dependencies: + os-tmpdir "~1.0.1" + tmp@0.0.31: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" @@ -23456,7 +23491,7 @@ xml-parse-from-string@^1.0.0: resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28" integrity sha1-qQKekp09vN7RafPG4oI42VpdWig= -xml2js@^0.4.19, xml2js@^0.4.5: +xml2js@^0.4.17, xml2js@^0.4.19, xml2js@^0.4.5: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==