diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 4c71693dd4125..2c5b8bff30d47 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -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(); } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 0a1798442f360..4172e7087ea36 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -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; @@ -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} */ - public async getCurrentUrl() { + public async getCurrentUrl(relativeUrl: boolean = false): Promise { // 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}`); + } } /** diff --git a/test/plugin_functional/test_suites/core_plugins/application_deep_links.ts b/test/plugin_functional/test_suites/core_plugins/application_deep_links.ts index e4973b05bd955..1d326bcdfef82 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_deep_links.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_deep_links.ts @@ -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'); @@ -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); @@ -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 () => { @@ -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'); }); diff --git a/test/plugin_functional/test_suites/core_plugins/application_leave_confirm.ts b/test/plugin_functional/test_suites/core_plugins/application_leave_confirm.ts index 4d0f837108b73..5ec44365f7a64 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_leave_confirm.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_leave_confirm.ts @@ -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 => { - 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'); }); }); }); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index b780ef125b71b..862cb6acfb6df 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -6,24 +6,17 @@ * Side Public License, v 1. */ -import url from 'url'; import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; -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 testSubjects = getService('testSubjects'); const find = getService('find'); - const retry = getService('retry'); const deployment = getService('deployment'); const esArchiver = getService('esArchiver'); - const log = getService('log'); - - function waitUntilLoadingIsDone() { - return PageObjects.header.waitUntilLoadingHasFinished(); - } const loadingScreenNotShown = async () => expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false); @@ -33,33 +26,6 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide return (await wrapper.getSize()).height; }; - 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, - }); - - async function navigateToAppFromAppsMenu(title: string) { - await retry.try(async () => { - await appsMenu.clickLink(title); - await waitUntilLoadingIsDone(); - }); - } - - /** 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(); - if (currentUrl !== expectedUrl) - log.debug(`expected url to be ${expectedUrl}, got ${currentUrl}`); - return currentUrl === expectedUrl; - }); - }; - const navigateTo = async (path: string) => await browser.navigateTo(`${deployment.getHostPort()}${path}`); @@ -67,8 +33,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide describe.skip('ui applications', function describeIndexTests() { before(async () => { await esArchiver.emptyKibanaIndex(); - await PageObjects.common.navigateToApp('foo'); - await PageObjects.common.dismissBanner(); + await common.navigateToApp('foo'); + await common.dismissBanner(); }); it('starts on home page', async () => { @@ -77,40 +43,37 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide it('redirects and renders correctly regardless of trailing slash', async () => { await navigateTo(`/app/foo`); - await waitForUrlToBe('/app/foo/home'); + await browser.waitForUrlToBe('/app/foo/home'); await testSubjects.existOrFail('fooAppHome'); await navigateTo(`/app/foo/`); - await waitForUrlToBe('/app/foo/home'); + await browser.waitForUrlToBe('/app/foo/home'); await testSubjects.existOrFail('fooAppHome'); }); it('navigates to its own pages', async () => { // Go to page A await testSubjects.click('fooNavPageA'); - await waitForUrlToBe('/app/foo/page-a'); + await browser.waitForUrlToBe('/app/foo/page-a'); await loadingScreenNotShown(); await testSubjects.existOrFail('fooAppPageA'); // Go to home page await testSubjects.click('fooNavHome'); - await waitForUrlToBe('/app/foo/home'); + await browser.waitForUrlToBe('/app/foo/home'); await loadingScreenNotShown(); await testSubjects.existOrFail('fooAppHome'); }); it('can use the back button to navigate within an app', async () => { await browser.goBack(); - await waitForUrlToBe('/app/foo/page-a'); + await browser.waitForUrlToBe('/app/foo/page-a'); await loadingScreenNotShown(); await testSubjects.existOrFail('fooAppPageA'); }); it('navigates to app root when navlink is clicked', async () => { - await testSubjects.click('fooNavHome'); - - navigateToAppFromAppsMenu('Foo'); - - await waitForUrlToBe('/app/foo/home'); + await appsMenu.clickLink('Foo', { category: 'kibana' }); + await browser.waitForUrlToBe('/app/foo/home'); await loadingScreenNotShown(); await testSubjects.existOrFail('fooAppHome'); }); @@ -119,7 +82,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide await testSubjects.click('fooNavBarPageB'); await loadingScreenNotShown(); await testSubjects.existOrFail('barAppPageB'); - await waitForUrlToBe('/app/bar/page-b', 'query=here'); + await browser.waitForUrlToBe('/app/bar/page-b?query=here'); }); it('preserves query parameters across apps', async () => { @@ -129,7 +92,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide it('can use the back button to navigate back to previous app', async () => { await browser.goBack(); - await waitForUrlToBe('/app/foo/home'); + await browser.waitForUrlToBe('/app/foo/home'); await loadingScreenNotShown(); await testSubjects.existOrFail('fooAppHome'); }); @@ -139,7 +102,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide }); it('navigating to chromeless application hides chrome', async () => { - await PageObjects.common.navigateToApp('chromeless'); + await common.navigateToApp('chromeless'); await loadingScreenNotShown(); expect(await testSubjects.exists('headerGlobalNav')).to.be(false); @@ -149,7 +112,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide }); it('navigating away from chromeless application shows chrome', async () => { - await PageObjects.common.navigateToApp('foo'); + await common.navigateToApp('foo'); await loadingScreenNotShown(); expect(await testSubjects.exists('headerGlobalNav')).to.be(true);