kibana/test/functional/page_objects/solution_navigation.ts
Maryam Saeidi f5c4f203fc
Attempt to fix flaky sidenav test (#212213)
Fixes #195153

## Summary

Waiting for the page to be loaded and the side nav item to exist before
clicking.
2025-02-26 10:42:27 +01:00

394 lines
15 KiB
TypeScript

/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import type { AppDeepLinkId } from '@kbn/core-chrome-browser';
import type { NavigationID as MlNavId } from '@kbn/default-nav-ml';
import type { NavigationID as AlNavId } from '@kbn/default-nav-analytics';
import type { NavigationID as MgmtNavId } from '@kbn/default-nav-management';
import type { NavigationID as DevNavId } from '@kbn/default-nav-devtools';
// use this for nicer type suggestions, but allow any string anyway
type NavigationId = MlNavId | AlNavId | MgmtNavId | DevNavId | string;
import type { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
import type { FtrProviderContext } from '../ftr_provider_context';
const getSectionIdTestSubj = (sectionId: NavigationId) => `~nav-item-${sectionId}`;
const TIMEOUT_CHECK = 3000;
export function SolutionNavigationProvider(ctx: Pick<FtrProviderContext, 'getService'>) {
const testSubjects = ctx.getService('testSubjects');
const browser = ctx.getService('browser');
const retry = ctx.getService('retry');
const log = ctx.getService('log');
async function getByVisibleText(
selector: string | (() => Promise<WebElementWrapper[]>),
text: string
) {
const subjects =
typeof selector === 'string' ? await testSubjects.findAll(selector) : await selector();
let found: WebElementWrapper | null = null;
for (const subject of subjects) {
const visibleText = await subject.getVisibleText();
if (visibleText === text) {
found = subject;
break;
}
}
return found;
}
return {
// check that chrome ui is in project/solution mode
async expectExists() {
await testSubjects.existOrFail('kibanaProjectHeader');
},
async clickLogo() {
await testSubjects.click('nav-header-logo');
},
// side nav related actions
sidenav: {
async expectLinkExists(
by:
| { deepLinkId: AppDeepLinkId }
| { navId: string }
| { text: string }
| { panelNavLinkId: string }
) {
if ('deepLinkId' in by) {
await testSubjects.existOrFail(`~nav-item-deepLinkId-${by.deepLinkId}`, {
timeout: TIMEOUT_CHECK,
});
} else if ('navId' in by) {
await testSubjects.existOrFail(`~nav-item-id-${by.navId}`, { timeout: TIMEOUT_CHECK });
} else if ('panelNavLinkId' in by) {
await testSubjects.existOrFail(`~panelNavItem-id-${by.panelNavLinkId}`, {
timeout: TIMEOUT_CHECK,
});
} else {
expect(await getByVisibleText('~nav-item', by.text)).not.be(null);
}
},
async expectLinkMissing(
by: { deepLinkId: AppDeepLinkId } | { navId: string } | { text: string }
) {
if ('deepLinkId' in by) {
await testSubjects.missingOrFail(`~nav-item-deepLinkId-${by.deepLinkId}`, {
timeout: TIMEOUT_CHECK,
});
} else if ('navId' in by) {
await testSubjects.missingOrFail(`~nav-item-id-${by.navId}`, { timeout: TIMEOUT_CHECK });
} else {
expect(await getByVisibleText('~nav-item', by.text)).be(null);
}
},
async expectLinkActive(
by: { deepLinkId: AppDeepLinkId } | { navId: string } | { text: string }
) {
await this.expectLinkExists(by);
if ('deepLinkId' in by) {
await testSubjects.existOrFail(
`~nav-item-deepLinkId-${by.deepLinkId} & ~nav-item-isActive`,
{ timeout: TIMEOUT_CHECK }
);
} else if ('navId' in by) {
await testSubjects.existOrFail(`~nav-item-id-${by.navId} & ~nav-item-isActive`, {
timeout: TIMEOUT_CHECK,
});
} else {
await retry.try(async () => {
const link = await getByVisibleText('~nav-item', by.text);
expect(await link!.elementHasClass(`nav-item-isActive`)).to.be(true);
});
}
},
async clickLink(by: { deepLinkId: AppDeepLinkId } | { navId: string } | { text: string }) {
await this.expectLinkExists(by);
if ('deepLinkId' in by) {
await testSubjects.existOrFail(`~nav-item-deepLinkId-${by.deepLinkId}`);
await testSubjects.click(`~nav-item-deepLinkId-${by.deepLinkId}`);
} else if ('navId' in by) {
await testSubjects.existOrFail(`~nav-item-id-${by.navId}`);
await testSubjects.click(`~nav-item-id-${by.navId}`);
} else {
await retry.try(async () => {
const link = await getByVisibleText('~nav-item', by.text);
await link!.click();
});
}
},
async findLink(by: { deepLinkId: AppDeepLinkId } | { navId: string } | { text: string }) {
await this.expectLinkExists(by);
if ('deepLinkId' in by) {
return testSubjects.find(`~nav-item-deepLinkId-${by.deepLinkId}`);
} else if ('navId' in by) {
return testSubjects.find(`~nav-item-id-${by.navId}`);
} else {
return retry.try(async () => {
const link = await getByVisibleText('~nav-item', by.text);
return link;
});
}
},
async expectOnlyDefinedLinks(navItemIds: string[]) {
const navItemIdRegEx = /nav-item-id-[^\s]+/g;
const allSideNavLinks = await testSubjects.findAll('*nav-item-id-');
for (const sideNavItem of allSideNavLinks) {
const dataTestSubjs = await sideNavItem.getAttribute('data-test-subj');
const navItemIdMatch = dataTestSubjs?.match(navItemIdRegEx);
expect(navItemIdMatch).to.be.ok();
const navItemId = navItemIdMatch![0].replace('nav-item-id-', '');
expect(navItemIds).to.contain(navItemId);
}
expect(allSideNavLinks.length).to.equal(navItemIds.length);
},
async clickPanelLink(deepLinkId: string) {
await testSubjects.click(`~panelNavItem-id-${deepLinkId}`);
},
async expectSectionExists(sectionId: NavigationId) {
log.debug('SolutionNavigation.sidenav.expectSectionExists', sectionId);
await testSubjects.existOrFail(getSectionIdTestSubj(sectionId), { timeout: TIMEOUT_CHECK });
},
async isSectionOpen(sectionId: NavigationId) {
await this.expectSectionExists(sectionId);
const collapseBtn = await testSubjects.find(`~accordionArrow-${sectionId}`);
const isExpanded = await collapseBtn.getAttribute('aria-expanded');
return isExpanded === 'true';
},
async expectSectionOpen(sectionId: NavigationId) {
log.debug('SolutionNavigation.sidenav.expectSectionOpen', sectionId);
await this.expectSectionExists(sectionId);
await retry.waitFor(`section ${sectionId} to be open`, async () => {
const isOpen = await this.isSectionOpen(sectionId);
return isOpen;
});
},
async expectSectionClosed(sectionId: NavigationId) {
await this.expectSectionExists(sectionId);
await retry.waitFor(`section ${sectionId} to be closed`, async () => {
const isOpen = await this.isSectionOpen(sectionId);
return !isOpen;
});
},
async openSection(sectionId: NavigationId) {
log.debug('SolutionNavigation.sidenav.openSection', sectionId);
await this.expectSectionExists(sectionId);
const isOpen = await this.isSectionOpen(sectionId);
if (isOpen) return;
const collapseBtn = await testSubjects.find(`~accordionArrow-${sectionId}`, TIMEOUT_CHECK);
await collapseBtn.click();
await this.expectSectionOpen(sectionId);
},
async closeSection(sectionId: NavigationId) {
await this.expectSectionExists(sectionId);
const isOpen = await this.isSectionOpen(sectionId);
if (!isOpen) return;
const collapseBtn = await testSubjects.find(`~accordionArrow-${sectionId}`, TIMEOUT_CHECK);
await collapseBtn.click();
await this.expectSectionClosed(sectionId);
},
async expectPanelExists(sectionId: NavigationId) {
log.debug('SolutionNavigation.sidenav.expectPanelExists', sectionId);
await testSubjects.existOrFail(`~sideNavPanel-id-${sectionId}`, {
timeout: TIMEOUT_CHECK,
});
},
async isPanelOpen(sectionId: NavigationId) {
try {
const panel = await testSubjects.find(`~sideNavPanel-id-${sectionId}`, TIMEOUT_CHECK);
return !!panel;
} catch (err) {
return false;
}
},
async openPanel(
sectionId: NavigationId,
{ button }: { button: 'icon' | 'link' } = { button: 'icon' }
) {
log.debug('SolutionNavigation.sidenav.openPanel', sectionId);
const isOpen = await this.isPanelOpen(sectionId);
if (isOpen) return;
const panelOpenerBtn = await testSubjects.find(
button === 'icon' ? `~panelOpener-id-${sectionId}` : `~nav-item-id-${sectionId}`,
TIMEOUT_CHECK
);
await panelOpenerBtn.click();
},
async closePanel(
sectionId: NavigationId,
{ button }: { button: 'icon' | 'link' } = { button: 'icon' }
) {
log.debug('SolutionNavigation.sidenav.closePanel', sectionId);
const isOpen = await this.isPanelOpen(sectionId);
if (!isOpen) return;
const panelOpenerBtn = await testSubjects.find(
button === 'icon' ? `~panelOpener-id-${sectionId}` : `~nav-item-id-${sectionId}`,
TIMEOUT_CHECK
);
await panelOpenerBtn.click();
},
async isCollapsed() {
const collapseNavBtn = await testSubjects.find('euiCollapsibleNavButton', TIMEOUT_CHECK);
return (await collapseNavBtn.getAttribute('aria-expanded')) === 'false';
},
async isExpanded() {
return !(await this.isCollapsed());
},
/**
* Toggles collapsed state of sidenav
*/
async toggle(collapsed?: boolean) {
const currentlyCollapsed = await this.isCollapsed();
const shouldBeCollapsed = collapsed ?? !currentlyCollapsed;
if (currentlyCollapsed !== shouldBeCollapsed) {
log.debug(
'SolutionNavigation.sidenav.toggle',
shouldBeCollapsed ? 'Collapsing' : 'Expanding'
);
const collapseNavBtn = await testSubjects.find('euiCollapsibleNavButton', TIMEOUT_CHECK);
await collapseNavBtn.click();
}
},
feedbackCallout: {
async expectExists() {
await testSubjects.existOrFail('sideNavfeedbackCallout', { timeout: TIMEOUT_CHECK });
},
async expectMissing() {
await testSubjects.missingOrFail('sideNavfeedbackCallout', { timeout: TIMEOUT_CHECK });
},
async dismiss() {
await testSubjects.click('sideNavfeedbackCallout > euiDismissCalloutButton');
},
},
},
breadcrumbs: {
async expectExists() {
await testSubjects.existOrFail('breadcrumbs', { timeout: TIMEOUT_CHECK });
},
async clickBreadcrumb(by: { deepLinkId: AppDeepLinkId } | { text: string }) {
if ('deepLinkId' in by) {
await testSubjects.click(`~breadcrumb-deepLinkId-${by.deepLinkId}`);
} else {
await (await getByVisibleText('~breadcrumb', by.text))?.click();
}
},
getBreadcrumb(by: { deepLinkId: AppDeepLinkId } | { text: string }) {
if ('deepLinkId' in by) {
return testSubjects.find(`~breadcrumb-deepLinkId-${by.deepLinkId}`, TIMEOUT_CHECK);
} else {
return getByVisibleText('~breadcrumb', by.text);
}
},
async expectBreadcrumbExists(by: { deepLinkId: AppDeepLinkId } | { text: string }) {
log.debug('SolutionNavigation.breadcrumbs.expectBreadcrumbExists', JSON.stringify(by));
if ('deepLinkId' in by) {
await testSubjects.existOrFail(`~breadcrumb-deepLinkId-${by.deepLinkId}`, {
timeout: TIMEOUT_CHECK,
});
} else {
await retry.try(async () => {
expect(await getByVisibleText('~breadcrumb', by.text)).not.be(null);
});
}
},
async expectBreadcrumbMissing(by: { deepLinkId: AppDeepLinkId } | { text: string }) {
if ('deepLinkId' in by) {
await testSubjects.missingOrFail(`~breadcrumb-deepLinkId-${by.deepLinkId}`, {
timeout: TIMEOUT_CHECK,
});
} else {
await retry.try(async () => {
expect(await getByVisibleText('~breadcrumb', by.text)).be(null);
});
}
},
async expectBreadcrumbTexts(expectedBreadcrumbTexts: string[]) {
log.debug(
'SolutionNavigation.breadcrumbs.expectBreadcrumbTexts',
JSON.stringify(expectedBreadcrumbTexts)
);
await retry.try(async () => {
const breadcrumbsContainer = await testSubjects.find('breadcrumbs', TIMEOUT_CHECK);
const breadcrumbs = await breadcrumbsContainer.findAllByTestSubject('~breadcrumb');
breadcrumbs.shift(); // remove home
expect(expectedBreadcrumbTexts.length).to.eql(breadcrumbs.length);
const texts = await Promise.all(breadcrumbs.map((b) => b.getVisibleText()));
expect(expectedBreadcrumbTexts).to.eql(texts);
});
},
},
recent: {
async expectExists() {
await testSubjects.existOrFail('nav-item-recentlyAccessed', { timeout: TIMEOUT_CHECK });
},
async expectHidden() {
await testSubjects.missingOrFail('nav-item-recentlyAccessed', { timeout: TIMEOUT_CHECK });
},
async expectLinkExists(text: string) {
await this.expectExists();
let foundLink: WebElementWrapper | null = null;
await retry.try(async () => {
foundLink = await getByVisibleText(
async () =>
(
await testSubjects.find('nav-item-recentlyAccessed', TIMEOUT_CHECK)
).findAllByTagName('a'),
text
);
expect(!!foundLink).to.be(true);
});
return foundLink!;
},
async clickLink(text: string) {
const link = await this.expectLinkExists(text);
await link!.click();
},
},
// helper to assert that the page did not reload
async createNoPageReloadCheck() {
const trackReloadTs = Date.now();
await browser.execute(
({ ts }) => {
// @ts-ignore
window.__testTrackReload__ = ts;
},
{
ts: trackReloadTs,
}
);
return async () => {
const noReload = await browser.execute(
({ ts }) => {
// @ts-ignore
return window.__testTrackReload__ && window.__testTrackReload__ === ts;
},
{
ts: trackReloadTs,
}
);
expect(noReload).to.be(true);
};
},
};
}