From 6b9fa5fa5225a8d91c9554bde7fb0a7b5cde5d97 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 9 Sep 2022 10:22:39 -0500 Subject: [PATCH 1/2] [ftr/obs/alerts] refactor to avoid stale-element errors --- test/functional/services/common/find.ts | 4 +- test/functional/services/common/index.ts | 1 + .../services/common/retry_on_stale.ts | 44 ++++++--- test/functional/services/index.ts | 2 + .../services/observability/alerts/common.ts | 91 +++++++++---------- 5 files changed, 76 insertions(+), 66 deletions(-) diff --git a/test/functional/services/common/find.ts b/test/functional/services/common/find.ts index da12279a3ffa1..ec2d5385a0a43 100644 --- a/test/functional/services/common/find.ts +++ b/test/functional/services/common/find.ts @@ -10,7 +10,6 @@ import { WebDriver, WebElement, By, until } from 'selenium-webdriver'; import { Browsers } from '../remote/browsers'; import { FtrService, FtrProviderContext } from '../../ftr_provider_context'; -import { retryOnStale } from './retry_on_stale'; import { WebElementWrapper } from '../lib/web_element_wrapper'; import { TimeoutOpt } from './types'; @@ -18,6 +17,7 @@ export class FindService extends FtrService { private readonly log = this.ctx.getService('log'); private readonly config = this.ctx.getService('config'); private readonly retry = this.ctx.getService('retry'); + private readonly retryOnStale = this.ctx.getService('retryOnStale'); private readonly WAIT_FOR_EXISTS_TIME = this.config.get('timeouts.waitForExists'); private readonly POLLING_TIME = 500; @@ -290,7 +290,7 @@ export class FindService extends FtrService { public async clickByCssSelectorWhenNotDisabled(selector: string, opts?: TimeoutOpt) { const timeout = opts?.timeout ?? this.defaultFindTimeout; - await retryOnStale(this.log, async () => { + await this.retryOnStale(async () => { this.log.debug(`Find.clickByCssSelectorWhenNotDisabled(${selector}, timeout=${timeout})`); const element = await this.byCssSelector(selector); diff --git a/test/functional/services/common/index.ts b/test/functional/services/common/index.ts index b7b8c67a4280d..54c9e5a1ee54c 100644 --- a/test/functional/services/common/index.ts +++ b/test/functional/services/common/index.ts @@ -14,3 +14,4 @@ export { PngService } from './png'; export { ScreenshotsService } from './screenshots'; export { SnapshotsService } from './snapshots'; export { TestSubjects } from './test_subjects'; +export { RetryOnStaleProvider } from './retry_on_stale'; diff --git a/test/functional/services/common/retry_on_stale.ts b/test/functional/services/common/retry_on_stale.ts index a240e8031cd68..4a190266458ec 100644 --- a/test/functional/services/common/retry_on_stale.ts +++ b/test/functional/services/common/retry_on_stale.ts @@ -6,30 +6,44 @@ * Side Public License, v 1. */ -import { ToolingLog } from '@kbn/tooling-log'; +import { FtrProviderContext } from '../../ftr_provider_context'; const MAX_ATTEMPTS = 10; const isObj = (v: unknown): v is Record => typeof v === 'object' && v !== null; const errMsg = (err: unknown) => (isObj(err) && typeof err.message === 'string' ? err.message : ''); -export async function retryOnStale(log: ToolingLog, fn: () => Promise): Promise { - let attempt = 0; - while (true) { - attempt += 1; - try { - return await fn(); - } catch (error) { - if (errMsg(error).includes('stale element reference')) { - if (attempt >= MAX_ATTEMPTS) { - throw new Error(`retryOnStale ran out of attempts after ${attempt} tries`); +export function RetryOnStaleProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + + async function retryOnStale(fn: () => Promise): Promise { + let attempt = 0; + while (true) { + attempt += 1; + try { + return await fn(); + } catch (error) { + if (errMsg(error).includes('stale element reference')) { + if (attempt >= MAX_ATTEMPTS) { + throw new Error(`retryOnStale ran out of attempts after ${attempt} tries`); + } + + log.warning('stale element exception caught, retrying'); + continue; } - log.warning('stale element exception caught, retrying'); - continue; + throw error; } - - throw error; } } + + retryOnStale.wrap = (fn: (...args: Args) => Promise) => { + return async (...args: Args) => { + return await retryOnStale(async () => { + return await fn(...args); + }); + }; + }; + + return retryOnStale; } diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 31d31e6424177..e2186ddefa5fa 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -17,6 +17,7 @@ import { ScreenshotsService, SnapshotsService, TestSubjects, + RetryOnStaleProvider, } from './common'; import { ComboBoxService } from './combo_box'; import { @@ -88,4 +89,5 @@ export const services = { managementMenu: ManagementMenuService, monacoEditor: MonacoEditorService, menuToggle: MenuToggleService, + retryOnStale: RetryOnStaleProvider, }; diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 6491c7a8b0595..7a3f1f609a403 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -36,6 +36,7 @@ export function ObservabilityAlertsCommonProvider({ const retry = getService('retry'); const toasts = getService('toasts'); const kibanaServer = getService('kibanaServer'); + const retryOnStale = getService('retryOnStale'); const navigateToTimeWithData = async () => { return await pageObjects.common.navigateToUrlWithBrowserHistory( @@ -108,14 +109,14 @@ export function ObservabilityAlertsCommonProvider({ return await find.allByCssSelector('.euiDataGridRowCell input[type="checkbox"]:enabled'); }; - const getTableCellsInRows = async () => { + const getTableCellsInRows = retryOnStale.wrap(async () => { const columnHeaders = await getTableColumnHeaders(); if (columnHeaders.length <= 0) { return []; } const cells = await getTableCells(); return chunk(cells, columnHeaders.length); - }; + }); const getTableOrFail = async () => { return await testSubjects.existOrFail(ALERTS_TABLE_CONTAINER_SELECTOR); @@ -134,37 +135,28 @@ export function ObservabilityAlertsCommonProvider({ return await testSubjects.find('queryInput'); }; - const getQuerySubmitButton = async () => { - return await testSubjects.find('querySubmitButton'); - }; - - const clearQueryBar = async () => { + const clearQueryBar = retryOnStale.wrap(async () => { return await (await getQueryBar()).clearValueWithKeyboard(); - }; + }); - const typeInQueryBar = async (query: string) => { + const typeInQueryBar = retryOnStale.wrap(async (query: string) => { return await (await getQueryBar()).type(query); - }; + }); const submitQuery = async (query: string) => { await typeInQueryBar(query); - return await (await getQuerySubmitButton()).click(); + await testSubjects.click('querySubmitButton'); }; // Flyout - const getViewAlertDetailsFlyoutButton = async () => { + const openAlertsFlyout = retryOnStale.wrap(async () => { await openActionsMenuForRow(0); - - return await testSubjects.find('viewAlertDetailsFlyout'); - }; - - const openAlertsFlyout = async () => { - await (await getViewAlertDetailsFlyoutButton()).click(); + await testSubjects.click('viewAlertDetailsFlyout'); await retry.waitFor( 'flyout open', async () => await testSubjects.exists(ALERTS_FLYOUT_SELECTOR, { timeout: 2500 }) ); - }; + }); const getAlertsFlyout = async () => { return await testSubjects.find(ALERTS_FLYOUT_SELECTOR); @@ -190,15 +182,19 @@ export function ObservabilityAlertsCommonProvider({ return await testSubjects.existOrFail('viewRuleDetailsFlyout'); }; - const getAlertsFlyoutDescriptionListTitles = async (): Promise => { - const flyout = await getAlertsFlyout(); - return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListTitle', flyout); - }; + const getAlertsFlyoutDescriptionListTitles = retryOnStale.wrap( + async (): Promise => { + const flyout = await getAlertsFlyout(); + return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListTitle', flyout); + } + ); - const getAlertsFlyoutDescriptionListDescriptions = async (): Promise => { - const flyout = await getAlertsFlyout(); - return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListDescription', flyout); - }; + const getAlertsFlyoutDescriptionListDescriptions = retryOnStale.wrap( + async (): Promise => { + const flyout = await getAlertsFlyout(); + return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListDescription', flyout); + } + ); // Cell actions @@ -210,17 +206,19 @@ export function ObservabilityAlertsCommonProvider({ return await testSubjects.find(FILTER_FOR_VALUE_BUTTON_SELECTOR); }; - const openActionsMenuForRow = async (rowIndex: number) => { + const openActionsMenuForRow = retryOnStale.wrap(async (rowIndex: number) => { const actionsOverflowButton = await getActionsButtonByIndex(rowIndex); await actionsOverflowButton.click(); - }; + }); const viewRuleDetailsButtonClick = async () => { - return await (await testSubjects.find(VIEW_RULE_DETAILS_SELECTOR)).click(); + await testSubjects.click(VIEW_RULE_DETAILS_SELECTOR); }; + const viewRuleDetailsLinkClick = async () => { - return await (await testSubjects.find(VIEW_RULE_DETAILS_FLYOUT_SELECTOR)).click(); + await testSubjects.click(VIEW_RULE_DETAILS_FLYOUT_SELECTOR); }; + // Workflow status const setWorkflowStatusForRow = async (rowIndex: number, workflowStatus: WorkflowStatus) => { await openActionsMenuForRow(rowIndex); @@ -236,17 +234,14 @@ export function ObservabilityAlertsCommonProvider({ await toasts.dismissAllToasts(); }; - const setWorkflowStatusFilter = async (workflowStatus: WorkflowStatus) => { - const buttonGroupButton = await testSubjects.find( - `workflowStatusFilterButton-${workflowStatus}` - ); - await buttonGroupButton.click(); - }; + const setWorkflowStatusFilter = retryOnStale.wrap(async (workflowStatus: WorkflowStatus) => { + await testSubjects.click(`workflowStatusFilterButton-${workflowStatus}`); + }); - const getWorkflowStatusFilterValue = async () => { + const getWorkflowStatusFilterValue = retryOnStale.wrap(async () => { const selectedWorkflowStatusButton = await find.byClassName('euiButtonGroupButton-isSelected'); return await selectedWorkflowStatusButton.getVisibleText(); - }; + }); // Alert status const setAlertStatusFilter = async (alertStatus?: AlertStatus) => { @@ -257,8 +252,8 @@ export function ObservabilityAlertsCommonProvider({ if (alertStatus === ALERT_STATUS_RECOVERED) { buttonSubject = 'alert-status-filter-recovered-button'; } - const buttonGroupButton = await testSubjects.find(buttonSubject); - await buttonGroupButton.click(); + + await testSubjects.click(buttonSubject); }; const alertDataIsBeingLoaded = async () => { @@ -277,14 +272,12 @@ export function ObservabilityAlertsCommonProvider({ const isAbsoluteRange = await testSubjects.exists('superDatePickerstartDatePopoverButton'); if (isAbsoluteRange) { - const startButton = await testSubjects.find('superDatePickerstartDatePopoverButton'); - const endButton = await testSubjects.find('superDatePickerendDatePopoverButton'); - return `${await startButton.getVisibleText()} - ${await endButton.getVisibleText()}`; + const startText = await testSubjects.getVisibleText('superDatePickerstartDatePopoverButton'); + const endText = await testSubjects.getVisibleText('superDatePickerendDatePopoverButton'); + return `${startText} - ${endText}`; } - const datePickerButton = await testSubjects.find('superDatePickerShowDatesButton'); - const buttonText = await datePickerButton.getVisibleText(); - return buttonText; + return await testSubjects.getVisibleText('superDatePickerShowDatesButton'); }; const getActionsButtonByIndex = async (index: number) => { @@ -294,14 +287,14 @@ export function ObservabilityAlertsCommonProvider({ return actionsOverflowButtons[index] || null; }; - const getRuleStatValue = async (testSubj: string) => { + const getRuleStatValue = retryOnStale.wrap(async (testSubj: string) => { const stat = await testSubjects.find(testSubj); const title = await stat.findByCssSelector('.euiStat__title'); const count = await title.getVisibleText(); const value = Number.parseInt(count, 10); expect(Number.isNaN(value)).to.be(false); return value; - }; + }); return { getQueryBar, From 8a2a9f68633bd99a05c3b5665497bff8716e43e8 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 9 Sep 2022 11:28:14 -0500 Subject: [PATCH 2/2] Revert "skip failing test suite (#140248)" This reverts commit b0b9b585fb1aa42a3078e67e6e8d1a3d3b68ae02. --- .../apps/observability/pages/alerts/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts index f2a59d6b22b2e..cdb0ea37a6417 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts @@ -20,8 +20,7 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const find = getService('find'); - // Failing: See https://github.com/elastic/kibana/issues/140248 - describe.skip('Observability alerts', function () { + describe('Observability alerts', function () { this.tags('includeFirefox'); const testSubjects = getService('testSubjects');