Improve ui applications navigation functional tests (#171314)

## Summary

Fix https://github.com/elastic/kibana/issues/53356

The PR brings a couple of main improvements:
* `apps_menu.clickLink()` the logic is sometimes not able to find the
right element "by partial link name". Adding a `category` parameter
helped narrow it down, but in some scenarios our applications are not
under any section, so we can't leverage this parameter. I have rewritten
the case where no `category` is specified, in order to fetch all side
nav links first, and then filter for the matching ones. This seems to
behave correctly 100% of times (at least confirmed by FTR below).
* Adds the extra `category` parameter for those scenarios where it can
be passed. It helps disambiguate when multiple links might match the
text (e.g. _Recent_), and reduces the odds of tests being flaky.

---

➡️ Flaky Test Runner Pipeline - 200x 🔴

https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4004

➡️ Rewrite
[apps_menu.clickLink()](5014947f6e)

➡️ Flaky Test Runner Pipeline - 150x 🟢 
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4011

➡️ CI
[failed](https://buildkite.com/elastic/kibana-pull-request/builds/176461),
so I pushed [this
commit](943d533156).

➡️ One last Flaky Test Runner Pipeline - 200x 🟢
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4028
This commit is contained in:
Gerard Soldevila 2023-11-22 18:20:16 +01:00 committed by GitHub
parent 543e8659d7
commit 1bc5b6f43e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 79 additions and 34 deletions

View file

@ -107,7 +107,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
"to:'2015-09-23T18:31:44.000Z'))&_a=(columns:!(),filters:!(),index:'logstash-*'," +
"interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))"
);
await appsMenu.clickLink('Discover');
await appsMenu.clickLink('Discover', { category: 'kibana' });
await PageObjects.header.waitUntilLoadingHasFinished();
expect(await filterBar.hasFilter('extension.raw', '', undefined, true)).to.be(true);
expect(await filterBar.isFilterPinned('extension.raw')).to.be(true);

View file

@ -36,13 +36,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const homeUrl = await browser.getCurrentUrl();
// Navigate to discover app
await appsMenu.clickLink('Discover');
await appsMenu.clickLink('Discover', { category: 'kibana' });
const discoverUrl = await browser.getCurrentUrl();
await PageObjects.timePicker.setDefaultAbsoluteRange();
const modifiedTimeDiscoverUrl = await browser.getCurrentUrl();
// Navigate to dashboard app
await appsMenu.clickLink('Dashboard');
await appsMenu.clickLink('Dashboard', { category: 'kibana' });
// Navigating back to discover
await browser.goBack();

View file

@ -116,23 +116,55 @@ export class AppsMenuService extends FtrService {
category,
}: { closeCollapsibleNav?: boolean; category?: string } = {}
) {
try {
this.log.debug(`click "${name}" app link`);
await this.openCollapsibleNav();
let nav;
if (typeof category === 'string') {
nav = await this.testSubjects.find(`collapsibleNavGroup-${category}`);
} else {
nav = await this.testSubjects.find('collapsibleNav');
}
const link = await nav.findByPartialLinkText(name);
await link.click();
await this.waitUntilLoadingHasFinished();
if (closeCollapsibleNav) {
await this.closeCollapsibleNav();
await this.ctx.getService('retry').try(async () => {
this.log.debug(`click "${name}" app link`);
try {
await this.openCollapsibleNav();
let nav;
if (typeof category === 'string') {
// we can search within a specific section of the side nav
nav = await this.testSubjects.find(`collapsibleNavGroup-${category}`);
const link = await nav.findByPartialLinkText(name);
await link.click();
} else {
// we need to search our app link in the whole side nav
// first, we get all the links, along with their inner text
const allLinks = await this.testSubjects.findAll('collapsibleNavAppLink');
const allLinksTexts = await Promise.all(
allLinks.map((link) => link.getVisibleText().then((text) => ({ link, text })))
);
// then, filter out those that don't have a matching text
const matchingLinks = allLinksTexts.filter(({ text }) => text.includes(name));
if (matchingLinks.length === 0) {
this.log.debug(
`Found ${allLinks.length} links on the side nav: ${allLinksTexts.map(
({ text }) => text
)}`
);
throw new Error(
`Could not find the '${name}' application on the side nav (${allLinks.length} links found)`
);
} else if (matchingLinks.length > 1) {
throw new Error(
`Multiple apps exist in the side nav with the specified name: '${name}'. Consider using the "category" parameter to disambiguate`
);
}
this.log.debug(`Found "${name}" app link on the side nav!`);
// if we click on a stale element (e.g. re-rendered) it'll throw an Error and we will retry
await matchingLinks.pop()!.link.click();
}
if (closeCollapsibleNav) {
await this.closeCollapsibleNav();
}
} finally {
// Intentionally empty
}
} finally {
// Intentionally empty
}
});
}
}

View file

@ -7,7 +7,7 @@
*/
import expect from '@kbn/expect';
import { PluginFunctionalProviderContext } from '../../services';
import type { PluginFunctionalProviderContext } from '../../services';
export default function ({ getService, getPageObject }: PluginFunctionalProviderContext) {
const common = getPageObject('common');
@ -17,10 +17,24 @@ export default function ({ getService, getPageObject }: PluginFunctionalProvider
const find = getService('find');
const deployment = getService('deployment');
const esArchiver = getService('esArchiver');
const log = getService('log');
const clickAppLink = async (app: string) => {
const appLink = `fooNav${app}`;
if (!(await testSubjects.exists(appLink))) {
log.debug(`App ${app} not found on side nav`);
}
await testSubjects.click(appLink);
};
const loadingScreenNotShown = async () =>
expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false);
const checkAppVisible = async (app: string) => {
const appContainer = `fooApp${app}`;
await testSubjects.existOrFail(appContainer);
};
const getAppWrapperHeight = async () => {
const wrapper = await find.byClassName('kbnAppWrapper');
return (await wrapper.getSize()).height;
@ -29,8 +43,7 @@ export default function ({ getService, getPageObject }: PluginFunctionalProvider
const navigateTo = async (path: string) =>
await browser.navigateTo(`${deployment.getHostPort()}${path}`);
// FLAKY: https://github.com/elastic/kibana/issues/53356
describe.skip('ui applications', function describeIndexTests() {
describe('ui applications', function describeIndexTests() {
before(async () => {
await esArchiver.emptyKibanaIndex();
await common.navigateToApp('foo');
@ -38,48 +51,48 @@ export default function ({ getService, getPageObject }: PluginFunctionalProvider
});
it('starts on home page', async () => {
await testSubjects.existOrFail('fooAppHome');
await checkAppVisible('Home');
});
it('redirects and renders correctly regardless of trailing slash', async () => {
await navigateTo(`/app/foo`);
await browser.waitForUrlToBe('/app/foo/home');
await testSubjects.existOrFail('fooAppHome');
await checkAppVisible('Home');
await navigateTo(`/app/foo/`);
await browser.waitForUrlToBe('/app/foo/home');
await testSubjects.existOrFail('fooAppHome');
await checkAppVisible('Home');
});
it('navigates to its own pages', async () => {
// Go to page A
await testSubjects.click('fooNavPageA');
await clickAppLink('PageA');
await browser.waitForUrlToBe('/app/foo/page-a');
await loadingScreenNotShown();
await testSubjects.existOrFail('fooAppPageA');
await checkAppVisible('PageA');
// Go to home page
await testSubjects.click('fooNavHome');
await clickAppLink('Home');
await browser.waitForUrlToBe('/app/foo/home');
await loadingScreenNotShown();
await testSubjects.existOrFail('fooAppHome');
await checkAppVisible('Home');
});
it('can use the back button to navigate within an app', async () => {
await browser.goBack();
await browser.waitForUrlToBe('/app/foo/page-a');
await loadingScreenNotShown();
await testSubjects.existOrFail('fooAppPageA');
await checkAppVisible('PageA');
});
it('navigates to app root when navlink is clicked', async () => {
await appsMenu.clickLink('Foo', { category: 'kibana' });
await appsMenu.clickLink('Foo');
await browser.waitForUrlToBe('/app/foo/home');
await loadingScreenNotShown();
await testSubjects.existOrFail('fooAppHome');
await checkAppVisible('Home');
});
it('navigates to other apps', async () => {
await testSubjects.click('fooNavBarPageB');
await clickAppLink('BarPageB');
await loadingScreenNotShown();
await testSubjects.existOrFail('barAppPageB');
await browser.waitForUrlToBe('/app/bar/page-b?query=here');
@ -94,7 +107,7 @@ export default function ({ getService, getPageObject }: PluginFunctionalProvider
await browser.goBack();
await browser.waitForUrlToBe('/app/foo/home');
await loadingScreenNotShown();
await testSubjects.existOrFail('fooAppHome');
await checkAppVisible('Home');
});
it('chromeless applications are not visible in apps list', async () => {