Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
4 changes: 2 additions & 2 deletions test/functional/services/common/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ 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';

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;
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions test/functional/services/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
44 changes: 29 additions & 15 deletions test/functional/services/common/retry_on_stale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions test/functional/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ScreenshotsService,
SnapshotsService,
TestSubjects,
RetryOnStaleProvider,
} from './common';
import { ComboBoxService } from './combo_box';
import {
Expand Down Expand Up @@ -88,4 +89,5 @@ export const services = {
managementMenu: ManagementMenuService,
monacoEditor: MonacoEditorService,
menuToggle: MenuToggleService,
retryOnStale: RetryOnStaleProvider,
};
91 changes: 42 additions & 49 deletions x-pack/test/functional/services/observability/alerts/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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

Expand All @@ -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);
Expand All @@ -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) => {
Expand All @@ -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 () => {
Expand All @@ -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) => {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down