Skip to content

Commit

Permalink
Rework application_leave_confirm functional tests (#170449)
Browse files Browse the repository at this point in the history
## Summary

Reworks #166838 for better
readability and maintainability.
Refactors `waitForUrlToBe`, moving it as part of the `browser.ts` API.

Flaky test runer pipeline - 100x ⌛ 

https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3879
🔴 ➡️
be19f7e

https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3884
🟢
  • Loading branch information
gsoldevila authored Nov 10, 2023
1 parent 4b28ec4 commit 6c5ffd2
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 190 deletions.
13 changes: 2 additions & 11 deletions test/functional/page_objects/common_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,18 +417,9 @@ export class CommonPageObject extends FtrService {
* Clicks cancel button on modal
* @param overlayWillStay pass in true if your test will show multiple modals in succession
*/
async clickCancelOnModal(overlayWillStay = true, ignorePageLeaveWarning = false) {
async clickCancelOnModal(overlayWillStay = true) {
this.log.debug('Clicking modal cancel');
await this.testSubjects.exists('confirmModalTitleText');

await this.retry.try(async () => {
const warning = await this.testSubjects.exists('confirmModalTitleText');
if (warning) {
await this.testSubjects.click(
ignorePageLeaveWarning ? 'confirmModalConfirmButton' : 'confirmModalCancelButton'
);
}
});
await this.testSubjects.click('confirmModalCancelButton');
if (!overlayWillStay) {
await this.ensureModalOverlayHidden();
}
Expand Down
53 changes: 47 additions & 6 deletions test/functional/services/common/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@
* Side Public License, v 1.
*/

import Url from 'url';
import { setTimeout as setTimeoutAsync } from 'timers/promises';
import { cloneDeepWith, isString } from 'lodash';
import { Key, Origin, WebDriver } from 'selenium-webdriver';
import { Key, Origin, type WebDriver } from 'selenium-webdriver';
import { Driver as ChromiumWebDriver } from 'selenium-webdriver/chrome';
import { modifyUrl } from '@kbn/std';

import sharp from 'sharp';
import { NoSuchSessionError } from 'selenium-webdriver/lib/error';
import { WebElementWrapper } from '../lib/web_element_wrapper';
import { FtrProviderContext, FtrService } from '../../ftr_provider_context';
import { type FtrProviderContext, FtrService } from '../../ftr_provider_context';
import { Browsers } from '../remote/browsers';
import { NetworkOptions, NetworkProfile, NETWORK_PROFILES } from '../remote/network_profiles';
import {
type NetworkOptions,
type NetworkProfile,
NETWORK_PROFILES,
} from '../remote/network_profiles';

export type Browser = BrowserService;

Expand Down Expand Up @@ -164,17 +169,53 @@ class BrowserService extends FtrService {
/**
* Gets the URL that is loaded in the focused window/frame.
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getCurrentUrl
*
* @param relativeUrl (optional) set to true to return the relative URL (without the hostname and protocol)
* @return {Promise<string>}
*/
public async getCurrentUrl() {
public async getCurrentUrl(relativeUrl: boolean = false): Promise<string> {
// strip _t=Date query param when url is read
const current = await this.driver.getCurrentUrl();
const currentWithoutTime = modifyUrl(current, (parsed) => {
delete (parsed.query as any)._t;
return void 0;
});
return currentWithoutTime;

if (relativeUrl) {
const { path } = Url.parse(currentWithoutTime);
return path!; // this property includes query params and anchors
} else {
return currentWithoutTime;
}
}

/**
* Uses the 'retry' service and waits for the current browser URL to match the provided path.
* NB the provided path can contain query params as well as hash anchors.
* Using retry logic makes URL assertions less flaky
* @param expectedPath The relative path that we are expecting the browser to be on
* @returns a Promise that will reject if the browser URL does not match the expected one
*/
public async waitForUrlToBe(expectedPath: string) {
const retry = await this.ctx.getService('retry');
const log = this.ctx.getService('log');

await retry.waitForWithTimeout(`URL to be ${expectedPath}`, 5000, async () => {
const currentPath = await this.getCurrentUrl(true);

if (currentPath !== expectedPath) {
log.debug(`Expected URL to be ${expectedPath}, got ${currentPath}`);
}
return currentPath === expectedPath;
});

// wait some time before checking the URL again
await new Promise((resolve) => setTimeout(resolve, 1000));

// ensure the URL stays the same and we did not go through any redirects
const currentPath = await this.getCurrentUrl(true);
if (currentPath !== expectedPath) {
throw new Error(`Expected URL to continue to be ${expectedPath}, got ${currentPath}`);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@
* Side Public License, v 1.
*/

import url from 'url';
import expect from '@kbn/expect';
import type { PluginFunctionalProviderContext } from '../../services';

export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const PageObjects = getPageObjects(['common']);
export default function ({ getService, getPageObject }: PluginFunctionalProviderContext) {
const common = getPageObject('common');
const browser = getService('browser');
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const log = getService('log');

Expand All @@ -27,25 +25,6 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
await testSubjects.click(appLink);
};

const getKibanaUrl = (pathname?: string, search?: string) =>
url.format({
protocol: 'http:',
hostname: process.env.TEST_KIBANA_HOST || 'localhost',
port: process.env.TEST_KIBANA_PORT || '5620',
pathname,
search,
});

/** Use retry logic to make URL assertions less flaky */
const waitForUrlToBe = (pathname?: string, search?: string) => {
const expectedUrl = getKibanaUrl(pathname, search);
return retry.waitFor(`Url to be ${expectedUrl}`, async () => {
const currentUrl = await browser.getCurrentUrl();
log?.debug(`waiting for currentUrl ${currentUrl} to be expectedUrl ${expectedUrl}`);
return currentUrl === expectedUrl;
});
};

const loadingScreenNotShown = async () =>
expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false);

Expand All @@ -57,7 +36,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
describe('application deep links navigation', function describeDeepLinksTests() {
before(async () => {
await esArchiver.emptyKibanaIndex();
await PageObjects.common.navigateToApp('dl');
await common.navigateToApp('dl');
});

it('should start on home page', async () => {
Expand All @@ -66,42 +45,42 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide

it('should navigate to page A when navlink is clicked', async () => {
await clickAppLink('PageA');
await waitForUrlToBe('/app/dl/page-a');
await browser.waitForUrlToBe('/app/dl/page-a');
await loadingScreenNotShown();
await checkAppVisible('PageA');
});

it('should be able to use the back button to navigate back to previous deep link', async () => {
await browser.goBack();
await waitForUrlToBe('/app/dl/home');
await browser.waitForUrlToBe('/app/dl/home');
await loadingScreenNotShown();
await checkAppVisible('Home');
});

it('should navigate to nested page B when navlink is clicked', async () => {
await clickAppLink('DeepPageB');
await waitForUrlToBe('/app/dl/page-b');
await browser.waitForUrlToBe('/app/dl/page-b');
await loadingScreenNotShown();
await checkAppVisible('PageB');
});

it('should navigate to Home when navlink is clicked inside the defined category group', async () => {
await clickAppLink('Home');
await waitForUrlToBe('/app/dl/home');
await browser.waitForUrlToBe('/app/dl/home');
await loadingScreenNotShown();
await checkAppVisible('Home');
});

it('should navigate to nested page B using navigateToApp path', async () => {
await clickAppLink('DeepPageB');
await waitForUrlToBe('/app/dl/page-b');
await browser.waitForUrlToBe('/app/dl/page-b');
await loadingScreenNotShown();
await checkAppVisible('PageB');
});

it('should navigate to nested page A using navigateToApp deepLinkId', async () => {
await clickAppLink('DeepPageAById');
await waitForUrlToBe('/app/dl/page-a');
await browser.waitForUrlToBe('/app/dl/page-a');
await loadingScreenNotShown();
await checkAppVisible('PageA');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,110 +6,34 @@
* Side Public License, v 1.
*/

import expect from '@kbn/expect';
import url from 'url';
import { PluginFunctionalProviderContext } from '../../services';
import type { PluginFunctionalProviderContext } from '../../services';

const getKibanaUrl = (pathname?: string, search?: string) =>
url.format({
protocol: 'http:',
hostname: process.env.TEST_KIBANA_HOST || 'localhost',
port: process.env.TEST_KIBANA_PORT || '5620',
pathname,
search,
});

export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const PageObjects = getPageObjects(['common', 'header']);
export default function ({ getService, getPageObject }: PluginFunctionalProviderContext) {
const common = getPageObject('common');
const browser = getService('browser');
const appsMenu = getService('appsMenu');
const log = getService('log');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const config = getService('config');

const waitForUrlToBe = async (pathname?: string, search?: string) => {
const expectedUrl = getKibanaUrl(pathname, search);
return await retry.waitFor(`Url to be ${expectedUrl}`, async () => {
const currentUrl = await browser.getCurrentUrl();
log.debug(`waiting for currentUrl ${currentUrl} to be expectedUrl ${expectedUrl}`);
return currentUrl === expectedUrl;
});
};

const ensureModalOpen = async (
defaultTryTimeout: number,
attempts: number,
timeMultiplier: number,
action: 'cancel' | 'confirm',
linkText: string = 'home'
): Promise<void> => {
let isConfirmCancelModalOpenState = false;

await retry.tryForTime(defaultTryTimeout * timeMultiplier, async () => {
await appsMenu.clickLink(linkText);
isConfirmCancelModalOpenState = await testSubjects.exists('confirmModalTitleText', {
allowHidden: true,
timeout: defaultTryTimeout * timeMultiplier,
});
});
if (isConfirmCancelModalOpenState) {
log.debug(`defaultTryTimeout * ${timeMultiplier} is long enough`);
return action === 'cancel'
? await PageObjects.common.clickCancelOnModal(true, false)
: await PageObjects.common.clickConfirmOnModal();
} else {
log.debug(`defaultTryTimeout * ${timeMultiplier} is not long enough`);
return await ensureModalOpen(
defaultTryTimeout,
(attempts = attempts > 0 ? attempts - 1 : 0),
(timeMultiplier = timeMultiplier < 10 ? timeMultiplier + 1 : 10),
action,
linkText
);
}
};

describe('application using leave confirmation', () => {
const defaultTryTimeout = config.get('timeouts.try');
const attempts = 5;
describe('when navigating to another app', () => {
const timeMultiplier = 10;
beforeEach(async () => {
await PageObjects.common.navigateToApp('home');
});
it('prevents navigation if user click cancel on the confirmation dialog', async () => {
await PageObjects.common.navigateToApp('appleave1');
await PageObjects.header.waitUntilLoadingHasFinished();
await waitForUrlToBe('/app/appleave1');
await common.navigateToApp('appleave1');
await browser.waitForUrlToBe('/app/appleave1');

await ensureModalOpen(defaultTryTimeout, attempts, timeMultiplier, 'cancel', 'AppLeave 2');
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.waitFor('navigate to appleave1', async () => {
const currentUrl = await browser.getCurrentUrl();
log.debug(`currentUrl ${currentUrl}`);
return currentUrl.includes('appleave1');
});
const currentUrl = await browser.getCurrentUrl();
expect(currentUrl).to.contain('appleave1');
await PageObjects.common.navigateToApp('home');
await appsMenu.clickLink('AppLeave 2', { category: 'kibana' });
await testSubjects.existOrFail('appLeaveConfirmModal');
await common.clickCancelOnModal(false);
await browser.waitForUrlToBe('/app/appleave1');
});

it('allows navigation if user click confirm on the confirmation dialog', async () => {
await PageObjects.common.navigateToApp('appleave1');
await PageObjects.header.waitUntilLoadingHasFinished();
await waitForUrlToBe('/app/appleave1');
await common.navigateToApp('appleave1');
await browser.waitForUrlToBe('/app/appleave1');

await ensureModalOpen(defaultTryTimeout, attempts, timeMultiplier, 'confirm', 'AppLeave 2');
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.waitFor('navigate to appleave1', async () => {
const currentUrl = await browser.getCurrentUrl();
log.debug(`currentUrl ${currentUrl}`);
return currentUrl.includes('appleave2');
});
const currentUrl = await browser.getCurrentUrl();
expect(currentUrl).to.contain('appleave2');
await PageObjects.common.navigateToApp('home');
await appsMenu.clickLink('AppLeave 2', { category: 'kibana' });
await testSubjects.existOrFail('appLeaveConfirmModal');
await common.clickConfirmOnModal();
await browser.waitForUrlToBe('/app/appleave2');
});
});
});
Expand Down
Loading

0 comments on commit 6c5ffd2

Please sign in to comment.