Restore webdriver-based functional test runner driver (#31446) (#32846)

This commit is contained in:
Dmitry Lemeshko 2019-03-09 19:45:28 +01:00 committed by GitHub
parent 2c2637d4ee
commit 53ecc0b119
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1165 additions and 1067 deletions

View file

@ -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: {

View file

@ -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",

View file

@ -33,6 +33,7 @@ export default {
'<rootDir>/src/setup_node_env',
'<rootDir>/packages',
'<rootDir>/src/test_utils',
'<rootDir>/test/functional/services/remote',
],
collectCoverageFrom: [
'packages/kbn-ui-framework/src/components/**/*.js',

View file

@ -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,

View file

@ -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);

View file

@ -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 });

View file

@ -306,9 +306,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) {
@ -351,12 +351,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);
@ -370,6 +369,9 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
}
await this.clickSave();
if (saveOptions.waitDialogIsClosed) {
await testSubjects.waitForDeleted(modalDialog);
}
}
async selectDashboard(dashName) {

View file

@ -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 } }
);
}

View file

@ -320,7 +320,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 ? '*' : ''}`);

View file

@ -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() {

View file

@ -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');

View file

@ -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() {

View file

@ -653,13 +653,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 } = {}) {

View file

@ -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<void>}
*/
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<string>}
*/
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 environments 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<void>}
*/
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<void>}
*/
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<void>}
*/
async refresh() {
await leadfoot.refresh();
await driver.navigate().refresh();
}
/**
* Navigates the focused window/frame back one page using the browsers 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<void>}
*/
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<void>}
*/
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<void>}
*/
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<string>}
*/
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<LogEntry[]>}
*/
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<Buffer>}
*/
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<void>}
*/
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<void>}
*/
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<string[]>}
*/
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<void>}
*/
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<void>}
*/
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;
}
}));
}

View file

@ -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) {

View file

@ -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
}
);
}

View file

@ -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);
});
}
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;
});
});
}
async allByLinkText(selector, timeout = defaultFindTimeout) {
log.debug('find.allByLinkText: ' + selector);
return await this.allByCustom(
async leadfoot => wrapAll(await leadfoot.findAllByLinkText(selector)),
timeout
);
}
async allByCssSelector(selector, timeout = defaultFindTimeout) {
log.debug('in findAllByCssSelector: ' + selector);
return await this.allByCustom(
async leadfoot => wrapAll(await leadfoot.findAllByCssSelector(selector)),
timeout
);
}
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
);
}
async descendantDisplayedByCssSelector(selector, parentElement) {
log.debug('Find.descendantDisplayedByCssSelector: ' + selector);
return await this._ensureElement(
async () => wrap(await parentElement.findDisplayedByCssSelector(selector))
);
}
async allDescendantDisplayedByCssSelector(selector, parentElement) {
log.debug(`Find.allDescendantDisplayedByCssSelector(${selector})`);
const allElements = await parentElement.findAllByCssSelector(selector);
return await Promise.all(
allElements.map((element) => (
this._ensureElement(async () => wrap(element))
))
);
}
async displayedByCssSelector(selector, timeout = defaultFindTimeout) {
log.debug('in displayedByCssSelector: ' + selector);
return await this._ensureElementWithTimeout(timeout, async leadfoot => {
return wrap(await leadfoot.findDisplayedByCssSelector(selector));
});
}
async byLinkText(selector, timeout = defaultFindTimeout) {
log.debug('Find.byLinkText: ' + selector);
return await this._ensureElementWithTimeout(timeout, async leadfoot => {
return wrap(await leadfoot.findByLinkText(selector));
});
}
async findDisplayedByLinkText(selector, timeout = defaultFindTimeout) {
log.debug('Find.byLinkText: ' + selector);
return await this._ensureElementWithTimeout(timeout, async leadfoot => {
return wrap(await leadfoot.findDisplayedByLinkText(selector));
});
}
async byPartialLinkText(partialLinkText, timeout = defaultFindTimeout) {
log.debug(`find.byPartialLinkText(${partialLinkText})`);
return await this._ensureElementWithTimeout(timeout, async leadfoot => {
return wrap(await leadfoot.findByPartialLinkText(partialLinkText));
});
}
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;
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) {
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}') 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(`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}') 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}')`);
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 wrapAll(await parentElement._webElement.findElements(By.css(selector)));
return await this.filterElementIsDisplayed(allElements);
}
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 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 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}') with timeout=${timeout}`);
return wrap(await driver.wait(until.elementLocated(By.partialLinkText(partialLinkText)), timeout));
}
async exists(findFunction, timeout = WAIT_FOR_EXISTS_TIME) {
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();

View file

@ -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,

View file

@ -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';

View file

@ -1,342 +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';
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<void>}
*/
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<LeadfootElementWrapper[]>}
*/
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<void>}
*/
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<void>}
*/
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<LeadfootElementWrapper>}
*/
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<boolean>}
*/
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. <br> 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<string>}
*/
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<string>}
*/
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<LeadfootElementWrapper[]>}
*/
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<LeadfootElementWrapper>}
*/
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<boolean>}
*/
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<LeadfootElementWrapper[]>}
*/
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<any>}
*/
async getProperty(name) {
return await this._leadfootElement.getProperty(name);
}
/**
* Moves the remote environments 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 elements 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 elements bounding client rectangle.
* @return {Promise<void>}
*/
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<string>}
*/
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<LeadfootElementWrapper>}
*/
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<boolean>}
*/
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<LeadfootElementWrapper>}
*/
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<void>}
*/
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<LeadfootElementWrapper>}
*/
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<LeadfootElementWrapper>}
*/
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<void>}
*/
async pressKeys(...args) {
await this._leadfoot.pressKeys(...args);
}
/**
* Scroll the element into view, avoiding the fixed header if necessary
*
* @nonstandard
* @return {Promise<void>}
*/
async scrollIntoViewIfNecessary() {
await this._leadfoot.execute(scrollIntoViewIfNecessary, [this._leadfootElement, this._fixedHeaderHeight]);
}
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { BrowserDriverApi } from './browser_driver_api';
export { WebElementWrapper } from './web_element_wrapper';

View file

@ -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<boolean>}
*/
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<boolean>}
*/
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<boolean>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<any>}
*/
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<string>}
*/
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<string>}
*/
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<string>}
*/
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 environments 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<void>}
*/
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<WebElementWrapper>}
*/
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<WebElementWrapper[]>}
*/
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<WebElementWrapper>}
*/
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<WebElementWrapper[]>}
*/
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<WebElementWrapper>}
*/
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<WebElementWrapper[]>}
*/
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<WebElementWrapper>}
*/
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<WebElementWrapper[]>}
*/
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<WebElementWrapper[]>}
*/
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<WebElementWrapper[]>}
*/
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<void>}
*/
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<void>}
*/
async scrollIntoViewIfNecessary() {
await this._driver.executeScript(scrollIntoViewIfNecessary, this._webElement, this._fixedHeaderHeight);
}
/**
* Gets element innerHTML and wrap it up with cheerio
*
* @nonstandard
* @return {Promise<void>}
*/
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 $;
}
}

View file

@ -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();
}
}

View file

@ -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));
}
});
}

View file

@ -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;
}
}

View file

@ -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;
}
})()
]);
}

View file

@ -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,
start() {
log.info(`Reusing instance at %j`, 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;
});
if (execQueue.push(task) === 1) {
// only item in the queue, kick it off
task.exec();
}
return task.promise;
};
}

View file

@ -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",
]
`);
});

View file

@ -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 };
}

View file

@ -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;
}
})()
]);
}

View file

@ -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);

View file

@ -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) {

View file

@ -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]);
},

View file

@ -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;
}

View file

@ -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();
});

View file

@ -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;
}

View file

@ -6356,6 +6356,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"
@ -7563,11 +7568,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"
@ -8152,6 +8152,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"
@ -11563,6 +11568,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=
immutability-helper@^2.0.0:
version "2.6.4"
resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.6.4.tgz#a931aef97257fcb6d2b5456de652ab6e3bba8408"
@ -13489,12 +13499,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"
@ -13749,14 +13763,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"
@ -13840,6 +13846,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"
@ -16543,11 +16556,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"
@ -18494,7 +18512,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=
@ -19626,6 +19644,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"
@ -21361,6 +21389,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"
@ -23850,7 +23885,7 @@ xml2js@0.2.8:
dependencies:
sax "0.5.x"
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==