mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
This commit is contained in:
parent
2c2637d4ee
commit
53ecc0b119
38 changed files with 1165 additions and 1067 deletions
|
@ -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: {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 } }
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ? '*' : ''}`);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 } = {}) {
|
||||
|
|
|
@ -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 environment’s mouse cursor to the specified element or relative
|
||||
* position. If the element is outside of the viewport, the remote driver will attempt
|
||||
* to scroll it into view automatically.
|
||||
* https://theintern.io/leadfoot/module-leadfoot_Session.html#moveMouseTo
|
||||
* position.
|
||||
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move
|
||||
*
|
||||
* @param {Element} element Optional
|
||||
* @param {WebElementWrapper} element Optional
|
||||
* @param {number} xOffset Optional
|
||||
* @param {number} yOffset Optional
|
||||
* @return {Promise<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 browser’s navigation history.
|
||||
* https://theintern.io/leadfoot/module-leadfoot_Session.html#goBack
|
||||
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#back
|
||||
*
|
||||
* @return {Promise<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;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
|
@ -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 environment’s mouse cursor to this element. If the element is outside
|
||||
* of the viewport, the remote driver will attempt to scroll it into view automatically.
|
||||
* https://theintern.io/leadfoot/module-leadfoot_Session.html#moveMouseTo
|
||||
*
|
||||
* @param {number} xOffset optional - The x-offset of the cursor, maybe in CSS pixels, relative to the left edge of the specified element’s bounding client rectangle.
|
||||
* @param {number} yOffset optional - The y-offset of the cursor, maybe in CSS pixels, relative to the top edge of the specified element’s bounding client rectangle.
|
||||
* @return {Promise<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]);
|
||||
}
|
||||
}
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { BrowserDriverApi } from './browser_driver_api';
|
||||
export { WebElementWrapper } from './web_element_wrapper';
|
|
@ -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 environment’s mouse cursor to the current element
|
||||
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move
|
||||
*
|
||||
* @return {Promise<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 $;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
})()
|
||||
]);
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
]
|
||||
`);
|
||||
});
|
|
@ -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 };
|
||||
}
|
||||
|
|
136
test/functional/services/remote/webdriver.js
Normal file
136
test/functional/services/remote/webdriver.js
Normal 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;
|
||||
}
|
||||
})()
|
||||
]);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]);
|
||||
},
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
12
x-pack/test/types/leadfoot.d.ts
vendored
12
x-pack/test/types/leadfoot.d.ts
vendored
|
@ -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;
|
||||
}
|
77
yarn.lock
77
yarn.lock
|
@ -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==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue