[ftr/obs/alerts] refactor to avoid stale-element errors (#140427)

* [ftr/obs/alerts] refactor to avoid stale-element errors

* Revert "skip failing test suite (#140248)"

This reverts commit b0b9b585fb.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Spencer 2022-09-12 08:04:51 -05:00 committed by GitHub
parent 01d28511e3
commit d42e0e1166
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 77 additions and 68 deletions

View file

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

View file

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

View file

@ -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<string, unknown> => typeof v === 'object' && v !== null;
const errMsg = (err: unknown) => (isObj(err) && typeof err.message === 'string' ? err.message : '');
export async function retryOnStale<T>(log: ToolingLog, fn: () => Promise<T>): Promise<T> {
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<T>(fn: () => Promise<T>): Promise<T> {
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 = <Args extends any[], Result>(fn: (...args: Args) => Promise<Result>) => {
return async (...args: Args) => {
return await retryOnStale(async () => {
return await fn(...args);
});
};
};
return retryOnStale;
}

View file

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

View file

@ -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<WebElementWrapper[]> => {
const flyout = await getAlertsFlyout();
return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListTitle', flyout);
};
const getAlertsFlyoutDescriptionListTitles = retryOnStale.wrap(
async (): Promise<WebElementWrapper[]> => {
const flyout = await getAlertsFlyout();
return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListTitle', flyout);
}
);
const getAlertsFlyoutDescriptionListDescriptions = async (): Promise<WebElementWrapper[]> => {
const flyout = await getAlertsFlyout();
return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListDescription', flyout);
};
const getAlertsFlyoutDescriptionListDescriptions = retryOnStale.wrap(
async (): Promise<WebElementWrapper[]> => {
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,

View file

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