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/testSubjects/clickOnEnabled] retry on stale-element exceptions #139964

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug(`expanded document id: ${expandDocId}`);

await dataGrid.clickRowToggle();
await find.clickByCssSelectorWhenNotDisabled('#kbn_doc_viewer_tab_1');
await find.clickByCssSelectorWhenNotDisabledWithoutRetry('#kbn_doc_viewer_tab_1');

await retry.waitForWithTimeout(
'document id in flyout matching the expanded document id',
Expand Down Expand Up @@ -140,7 +140,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug(`expanded document id: ${expandDocId}`);

await dataGrid.clickRowToggle();
await find.clickByCssSelectorWhenNotDisabled('#kbn_doc_viewer_tab_1');
await find.clickByCssSelectorWhenNotDisabledWithoutRetry('#kbn_doc_viewer_tab_1');

await retry.waitForWithTimeout(
'document id in flyout matching the expanded document id',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('interval errors', () => {
before(async () => {
// to trigger displaying of error messages
await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton');
await testSubjects.clickWhenNotDisabledWithoutRetry('visualizeEditorRenderButton');
// this will avoid issues with the play tooltip covering the interval field
await testSubjects.scrollIntoView('advancedParams-2');
});
Expand Down Expand Up @@ -520,7 +520,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visEditor.setInterval('Millisecond');

// Apply interval
await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton');
await testSubjects.clickWhenNotDisabledWithoutRetry('visualizeEditorRenderButton');

const isHelperScaledLabelExists = await find.existsByCssSelector(
'[data-test-subj="currentlyScaledText"]'
Expand All @@ -537,7 +537,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {

it('should update scaled label text after custom interval is set and time range is changed', async () => {
await PageObjects.visEditor.setInterval('10s', { type: 'custom' });
await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton');
await testSubjects.clickWhenNotDisabledWithoutRetry('visualizeEditorRenderButton');
const isHelperScaledLabelExists = await find.existsByCssSelector(
'[data-test-subj="currentlyScaledText"]'
);
Expand Down
2 changes: 1 addition & 1 deletion test/functional/page_objects/discover_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ export class DiscoverPageObject extends FtrService {
public async clickViewModeFieldStatsButton() {
await this.retry.tryForTime(2 * 1000, async () => {
await this.testSubjects.existOrFail('dscViewModeFieldStatsButton');
await this.testSubjects.clickWhenNotDisabled('dscViewModeFieldStatsButton');
await this.testSubjects.clickWhenNotDisabledWithoutRetry('dscViewModeFieldStatsButton');
await this.testSubjects.existOrFail('dscFieldStatsEmbeddedContent');
});
}
Expand Down
10 changes: 6 additions & 4 deletions test/functional/page_objects/settings_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export class SettingsPageObject extends FtrService {
}

async clearFieldTypeFilter(type: string) {
await this.testSubjects.clickWhenNotDisabled('indexedFieldTypeFilterDropdown');
await this.testSubjects.clickWhenNotDisabledWithoutRetry('indexedFieldTypeFilterDropdown');
await this.retry.try(async () => {
await this.testSubjects.existOrFail('indexedFieldTypeFilterDropdown-popover');
});
Expand All @@ -319,7 +319,7 @@ export class SettingsPageObject extends FtrService {
}

async setFieldTypeFilter(type: string) {
await this.testSubjects.clickWhenNotDisabled('indexedFieldTypeFilterDropdown');
await this.testSubjects.clickWhenNotDisabledWithoutRetry('indexedFieldTypeFilterDropdown');
await this.testSubjects.existOrFail('indexedFieldTypeFilterDropdown-popover');
await this.testSubjects.existOrFail(`indexedFieldTypeFilterDropdown-option-${type}`);
await this.testSubjects.click(`indexedFieldTypeFilterDropdown-option-${type}`);
Expand All @@ -328,7 +328,7 @@ export class SettingsPageObject extends FtrService {
}

async clearScriptedFieldLanguageFilter(type: string) {
await this.testSubjects.clickWhenNotDisabled('scriptedFieldLanguageFilterDropdown');
await this.testSubjects.clickWhenNotDisabledWithoutRetry('scriptedFieldLanguageFilterDropdown');
await this.retry.try(async () => {
await this.testSubjects.existOrFail('scriptedFieldLanguageFilterDropdown-popover');
});
Expand All @@ -344,7 +344,9 @@ export class SettingsPageObject extends FtrService {

async setScriptedFieldLanguageFilter(language: string) {
await this.retry.try(async () => {
await this.testSubjects.clickWhenNotDisabled('scriptedFieldLanguageFilterDropdown');
await this.testSubjects.clickWhenNotDisabledWithoutRetry(
'scriptedFieldLanguageFilterDropdown'
);
return await this.find.byCssSelector('div.euiPopover__panel[data-popover-open]');
});
await this.testSubjects.existOrFail('scriptedFieldLanguageFilterDropdown-popover');
Expand Down
2 changes: 1 addition & 1 deletion test/functional/page_objects/visual_builder_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export class VisualBuilderPageObject extends FtrService {
}

public async applyChanges() {
await this.testSubjects.clickWhenNotDisabled('applyBtn');
await this.testSubjects.clickWhenNotDisabledWithoutRetry('applyBtn');
}

/**
Expand Down
4 changes: 2 additions & 2 deletions test/functional/page_objects/visualize_editor_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class VisualizeEditorPageObject extends FtrService {
}

public async inputControlSubmit() {
await this.testSubjects.clickWhenNotDisabled('inputControlSubmitBtn');
await this.testSubjects.clickWhenNotDisabledWithoutRetry('inputControlSubmitBtn');
await this.visChart.waitForVisualizationRenderingStabilized();
}

Expand All @@ -70,7 +70,7 @@ export class VisualizeEditorPageObject extends FtrService {

const prevRenderingCount = await this.visChart.getVisualizationRenderingCount();
this.log.debug(`Before Rendering count ${prevRenderingCount}`);
await this.testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton');
await this.testSubjects.clickWhenNotDisabledWithoutRetry('visualizeEditorRenderButton');
await this.visChart.waitForRenderingCount(prevRenderingCount + 1);
}

Expand Down
27 changes: 23 additions & 4 deletions test/functional/services/common/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ 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');
Expand Down Expand Up @@ -285,16 +287,33 @@ export class FindService extends FtrService {
}, timeout);
}

public async clickByCssSelectorWhenNotDisabled(
public async clickByCssSelectorWhenNotDisabled(selector: string, opts?: TimeoutOpt) {
const timeout = opts?.timeout ?? this.defaultFindTimeout;

await retryOnStale(this.log, async () => {
this.log.debug(`Find.clickByCssSelectorWhenNotDisabled(${selector}, timeout=${timeout})`);

const element = await this.byCssSelector(selector);
await element.moveMouseTo();
await this.driver.wait(until.elementIsEnabled(element._webElement), timeout);
await element.click();
});
}

public async clickByCssSelectorWhenNotDisabledWithoutRetry(
selector: string,
{ timeout } = { timeout: this.defaultFindTimeout }
opts?: TimeoutOpt
): Promise<void> {
this.log.debug(`Find.clickByCssSelectorWhenNotDisabled('${selector}') with timeout=${timeout}`);
const timeout = opts?.timeout ?? this.defaultFindTimeout;

this.log.debug(
`Find.clickByCssSelectorWhenNotDisabledWithoutRetry(${selector}, 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);
const element = await this.byCssSelector(selector);
await element.moveMouseTo();
await this.driver.wait(until.elementIsEnabled(element._webElement), timeout);
await element.click();
Expand Down
35 changes: 35 additions & 0 deletions test/functional/services/common/retry_on_stale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { ToolingLog } from '@kbn/tooling-log';

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

log.warning('stale element exception caught, retrying');
continue;
}

throw error;
}
}
}
32 changes: 26 additions & 6 deletions test/functional/services/common/test_subjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import testSubjSelector from '@kbn/test-subj-selector';
import { WebElementWrapper } from '../lib/web_element_wrapper';
import { FtrService } from '../../ftr_provider_context';
import { TimeoutOpt } from './types';

interface ExistsOptions {
timeout?: number;
Expand Down Expand Up @@ -111,14 +112,33 @@ export class TestSubjects extends FtrService {
await input.type(text);
}

public async clickWhenNotDisabled(
/**
* Clicks on the element identified by the testSubject selector. If the retries
* automatically on "stale element" errors unlike clickWhenNotDisabledWithoutRetry.
* `opts.timeout` defaults to the 'timeouts.find' config, which defaults to 10 seconds
*/
public async clickWhenNotDisabled(selector: string, opts?: TimeoutOpt) {
this.log.debug(`TestSubjects.clickWhenNotDisabled(${selector})`);
await this.findService.clickByCssSelectorWhenNotDisabled(testSubjSelector(selector), opts);
}

/**
* Clicks on the element identified by the testSubject selector. Somewhat surprisingly,
* this method allows `stale element` errors to propogate, which is why it was renamed
* from `clickWhenNotDisabled()` and that method was re-implemented to be more consistent
* with the rest of the functions in this service.
*
* `opts.timeout` defaults to the 'timeouts.find' config, which defaults to 10 seconds
*/
public async clickWhenNotDisabledWithoutRetry(
selector: string,
{ timeout = this.FIND_TIME }: { timeout?: number } = {}
opts?: TimeoutOpt
): Promise<void> {
this.log.debug(`TestSubjects.clickWhenNotDisabled(${selector})`);
await this.findService.clickByCssSelectorWhenNotDisabled(testSubjSelector(selector), {
timeout,
});
this.log.debug(`TestSubjects.clickWhenNotDisabledWithoutRetry(${selector})`);
await this.findService.clickByCssSelectorWhenNotDisabledWithoutRetry(
testSubjSelector(selector),
opts
);
}

public async click(
Expand Down
11 changes: 11 additions & 0 deletions test/functional/services/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export interface TimeoutOpt {
timeout?: number;
}
4 changes: 2 additions & 2 deletions test/functional/services/dashboard/panel_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class DashboardPanelActionsService extends FtrService {
await this.openContextMenu();
const isActionVisible = await this.testSubjects.exists(EDIT_PANEL_DATA_TEST_SUBJ);
if (!isActionVisible) await this.clickContextMenuMoreItem();
await this.testSubjects.clickWhenNotDisabled(EDIT_PANEL_DATA_TEST_SUBJ);
await this.testSubjects.clickWhenNotDisabledWithoutRetry(EDIT_PANEL_DATA_TEST_SUBJ);
await this.header.waitUntilLoadingHasFinished();
await this.common.waitForTopNavToBeVisible();
}
Expand All @@ -92,7 +92,7 @@ export class DashboardPanelActionsService extends FtrService {
} else {
await this.openContextMenu();
}
await this.testSubjects.clickWhenNotDisabled(EDIT_PANEL_DATA_TEST_SUBJ);
await this.testSubjects.clickWhenNotDisabledWithoutRetry(EDIT_PANEL_DATA_TEST_SUBJ);
}

async clickExpandPanelToggle() {
Expand Down
8 changes: 6 additions & 2 deletions test/functional/services/field_editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,18 @@ export class FieldEditorService extends FtrService {
public async confirmSave() {
await this.retry.try(async () => {
await this.testSubjects.setValue('saveModalConfirmText', 'change');
await this.testSubjects.clickWhenNotDisabled('confirmModalConfirmButton', { timeout: 1000 });
await this.testSubjects.clickWhenNotDisabledWithoutRetry('confirmModalConfirmButton', {
timeout: 1000,
});
});
}

public async confirmDelete() {
await this.retry.try(async () => {
await this.testSubjects.setValue('deleteModalConfirmText', 'remove');
await this.testSubjects.clickWhenNotDisabled('confirmModalConfirmButton', { timeout: 1000 });
await this.testSubjects.clickWhenNotDisabledWithoutRetry('confirmModalConfirmButton', {
timeout: 1000,
});
});
}
}
2 changes: 1 addition & 1 deletion test/functional/services/filter_bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class FilterBarService extends FtrService {
}
}

await this.testSubjects.clickWhenNotDisabled('saveFilter');
await this.testSubjects.clickWhenNotDisabledWithoutRetry('saveFilter');
});
await this.header.awaitGlobalLoadingIndicatorHidden();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should start search, save session, restore session using "restore" button', async () => {
await comboBox.setCustom('dataViewSelector', 'logstash-*');
await comboBox.setCustom('searchMetricField', 'bytes');
await testSubjects.clickWhenNotDisabled('startSearch');
await testSubjects.clickWhenNotDisabledWithoutRetry('startSearch');
await testSubjects.find('searchResults-1');
await searchSessions.expectState('completed');
await searchSessions.save();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});

it('navigates to Discover app on action click carrying over pie slice filter', async () => {
await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
await testSubjects.clickWhenNotDisabledWithoutRetry(ACTION_TEST_SUBJ);
await discover.waitForDiscoverAppOnScreen();
await filterBar.hasFilter('memory', '160,000 to 200,000');
const filterCount = await filterBar.getFilterCount();
Expand Down Expand Up @@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});

it('navigates to Discover on click carrying over brushed time range', async () => {
await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
await testSubjects.clickWhenNotDisabledWithoutRetry(ACTION_TEST_SUBJ);
await discover.waitForDiscoverAppOnScreen();
const newTimeRangeDurationHours = await timePicker.getTimeDurationInHours();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});

it('navigates to Discover app to index pattern of the panel on action click', async () => {
await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
await testSubjects.clickWhenNotDisabledWithoutRetry(ACTION_TEST_SUBJ);
await discover.waitForDiscoverAppOnScreen();

const el = await testSubjects.find('discover-dataView-switch-link');
Expand All @@ -87,7 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboard.saveDashboard('Dashboard with Pie Chart');

await panelActions.openContextMenu();
await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ);
await testSubjects.clickWhenNotDisabledWithoutRetry(ACTION_TEST_SUBJ);
await discover.waitForDiscoverAppOnScreen();

const text = await timePicker.getShowDatesButtonText();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);

// check the JSON tab
await find.clickByCssSelectorWhenNotDisabled('#kbn_doc_viewer_tab_1');
await find.clickByCssSelectorWhenNotDisabledWithoutRetry('#kbn_doc_viewer_tab_1');
await retry.waitForWithTimeout(
'index in flyout JSON tab is matching the logstash index',
5000,
Expand Down
Loading