[performance] enable journeys on serverless - part 1 (#162902)

## Summary

This PR is the Step 1 to enable performance journeys run for serverless
projects.
The focus is to re-design journeys to be compatible both for stateful &
serverless Kibana.

I created `KibanaPage` class to have some shared UI actions across
different journeys.
`ProjectPage` extends `KibanaPage` and allows us to override actions,
that are different (or have different locators) in Kibana Project UI
(generic project at the moment)

`kibanaPage` is available in Step context and based on TEST_SERVERLESS
env var appropriate class instance is used.


```typescript
  .step('Go to Discover Page', async ({ page, kbnUrl, kibanaPage }) => {
    await page.goto(kbnUrl.get(`/app/discover`));
    await kibanaPage.waitForHeader();
    await page.waitForSelector('[data-test-subj="discoverDocTable"][data-render-complete="true"]');
    await page.waitForSelector(subj('globalLoadingIndicator-hidden'));
  })
```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dzmitry Lemechko 2023-08-02 12:59:23 +02:00 committed by GitHub
parent 2fd6af9063
commit 4dadcbb911
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 182 additions and 95 deletions

View file

@ -26,8 +26,11 @@ import { KibanaUrl } from '../services/kibana_url';
import { JourneyFtrHarness } from './journey_ftr_harness';
import { makeFtrConfigProvider } from './journey_ftr_config';
import { JourneyConfig, JourneyConfigOptions } from './journey_config';
import { KibanaPage } from '../services/page/kibana_page';
import { ProjectPage } from '../services/page/project_page';
export interface BaseStepCtx {
kibanaPage: KibanaPage | ProjectPage;
page: Page;
log: ToolingLog;
inputDelays: InputDelays;

View file

@ -22,7 +22,11 @@ export function makeFtrConfigProvider(
steps: AnyStep[]
): FtrConfigProvider {
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const configPath = config.getFtrConfigPath();
const isServerless = !!process.env.TEST_SERVERLESS;
// Use the same serverless FTR config for all journeys
const configPath = isServerless
? 'x-pack/test_serverless/shared/config.base.ts'
: config.getFtrConfigPath();
const defaultConfigPath = config.isXpack()
? 'x-pack/test/functional/config.base.js'
: 'test/functional/config.base.js';

View file

@ -25,6 +25,7 @@ import { KibanaUrl } from '../services/kibana_url';
import type { Step, AnyStep } from './journey';
import type { JourneyConfig } from './journey_config';
import { JourneyScreenshots } from './journey_screenshots';
import { getNewPageObject } from '../services/page';
export class JourneyFtrHarness {
private readonly screenshots: JourneyScreenshots;
@ -357,7 +358,11 @@ export class JourneyFtrHarness {
throw new Error('performance service is not properly initialized');
}
const isServerlessProject = !!this.config.get('serverless');
const kibanaPage = getNewPageObject(isServerlessProject, page, this.log);
this.#_ctx = this.journeyConfig.getExtendedStepCtx({
kibanaPage,
page,
log: this.log,
inputDelays: getInputDelays(),
@ -414,11 +419,8 @@ export class JourneyFtrHarness {
? args.map((arg) => (typeof arg === 'string' ? arg : inspect(arg, false, null))).join(' ')
: message.text();
if (
url.includes('kbn-ui-shared-deps-npm.dll.js') &&
text.includes('moment construction falls')
) {
// ignore errors from moment about constructing dates with invalid formats
if (url.includes('kbn-ui-shared-deps-npm.dll.js')) {
// ignore errors/warning from kbn-ui-shared-deps-npm.dll.js
return;
}

View file

@ -102,4 +102,8 @@ export class Auth {
public isCloud() {
return this.config.get('servers.kibana.hostname') !== 'localhost';
}
public isServerless() {
return !!this.config.get('serverless');
}
}

View file

@ -0,0 +1,16 @@
/*
* 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';
import { Page } from 'playwright';
import { KibanaPage } from './kibana_page';
import { ProjectPage } from './project_page';
export function getNewPageObject(isServerless: boolean, page: Page, log: ToolingLog) {
return isServerless ? new ProjectPage(page, log) : new KibanaPage(page, log);
}

View file

@ -0,0 +1,75 @@
/*
* 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 { subj } from '@kbn/test-subj-selector';
import { ToolingLog } from '@kbn/tooling-log';
import { Page } from 'playwright';
interface WaitForRenderArgs {
expectedItemsCount: number;
itemLocator: string;
checkAttribute: string;
}
export class KibanaPage {
readonly page: Page;
readonly log: ToolingLog;
constructor(page: Page, log: ToolingLog) {
this.page = page;
this.log = log;
}
async waitForHeader() {
return this.page.waitForSelector('.headerGlobalNav', {
state: 'attached',
});
}
async backToDashboardListing() {
await this.page.click(subj('breadcrumb dashboardListingBreadcrumb first'));
}
async waitForRender({ expectedItemsCount, itemLocator, checkAttribute }: WaitForRenderArgs) {
try {
await this.page.waitForFunction(
function renderCompleted(args: WaitForRenderArgs) {
const renderingItems = Array.from(document.querySelectorAll(args.itemLocator));
const allItemsLoaded = renderingItems.length === args.expectedItemsCount;
return allItemsLoaded
? renderingItems.every((e) => e.getAttribute(args.checkAttribute) === 'true')
: false;
},
{ expectedItemsCount, itemLocator, checkAttribute }
);
} catch (err) {
const loaded = await this.page.$$(itemLocator);
const rendered = await this.page.$$(`${itemLocator}[${checkAttribute}="true"]`);
this.log.error(
`'waitForRendering' failed: loaded - ${loaded.length}, rendered - ${rendered.length}, expected count - ${expectedItemsCount}`
);
throw err;
}
}
async waitForVisualizations(count: number) {
await this.waitForRender({
expectedItemsCount: count,
itemLocator: '[data-rendering-count]',
checkAttribute: 'data-render-complete',
});
}
async waitForCharts(count: number) {
await this.waitForRender({
expectedItemsCount: count,
itemLocator: '.echChartStatus',
checkAttribute: 'data-ech-render-complete',
});
}
}

View file

@ -0,0 +1,22 @@
/*
* 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 { subj } from '@kbn/test-subj-selector';
import { KibanaPage } from './kibana_page';
export class ProjectPage extends KibanaPage {
async waitForHeader() {
return this.page.waitForSelector(subj('kibanaProjectHeader'), {
state: 'attached',
});
}
async backToDashboardListing() {
await this.page.click(subj('nav-item-search_project_nav.explore.dashboards'));
}
}

View file

@ -17,6 +17,7 @@
"@kbn/tooling-log",
"@kbn/repo-info",
"@kbn/std",
"@kbn/test-subj-selector",
],
"exclude": [
"target/**/*",

View file

@ -6,10 +6,10 @@
*/
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { SynthtraceClient } from '../services/synthtrace';
import { generateData } from '../synthtrace_data/apm_data';
// FLAKY: https://github.com/elastic/kibana/issues/162813
export const journey = new Journey({
beforeSteps: async ({ kbnUrl, log, auth, es }) => {
// Install APM Package
@ -40,15 +40,15 @@ export const journey = new Journey({
await page.goto(kbnUrl.get(`app/apm/services`));
await page.waitForSelector(`[data-test-subj="serviceLink_nodejs"]`);
})
.step('Navigate to Service Overview Page', async ({ page, kbnUrl }) => {
await page.click(`[data-test-subj="serviceLink_nodejs"]`);
await page.waitForSelector(`[data-test-subj="apmMainTemplateHeaderServiceName"]`);
.step('Navigate to Service Overview Page', async ({ page }) => {
await page.click(subj('serviceLink_nodejs'));
await page.waitForSelector(subj('apmMainTemplateHeaderServiceName'));
})
.step('Navigate to Transactions tabs', async ({ page, kbnUrl }) => {
await page.click(`[data-test-subj="transactionsTab"]`);
await page.waitForSelector(`[data-test-subj="apmTransactionDetailLinkLink"]`);
.step('Navigate to Transactions tabs', async ({ page }) => {
await page.click(subj('transactionsTab'));
await page.waitForSelector(subj('apmTransactionDetailLinkLink'));
})
.step('Wait for Trace Waterfall on the page to load', async ({ page, kbnUrl }) => {
await page.click(`[data-test-subj="apmTransactionDetailLinkLink"]`);
await page.waitForSelector(`[data-test-subj="apmWaterfallButton"]`);
.step('Wait for Trace Waterfall on the page to load', async ({ page }) => {
await page.click(subj('apmTransactionDetailLinkLink'));
await page.waitForSelector(subj('apmWaterfallButton'));
});

View file

@ -7,6 +7,7 @@
import { Journey } from '@kbn/journeys';
import expect from '@kbn/expect';
import { subj } from '@kbn/test-subj-selector';
export const journey = new Journey({
beforeSteps: async ({ kibanaServer, retry }) => {
@ -47,5 +48,5 @@ export const journey = new Journey({
},
}).step('Go to cloud security dashboards Page', async ({ page, kbnUrl }) => {
await page.goto(kbnUrl.get(`/app/security/cloud_security_posture/dashboard`));
await page.waitForSelector(`[data-test-subj="csp:dashboard-sections-table-header-score"]`);
await page.waitForSelector(subj('csp:dashboard-sections-table-header-score'));
});

View file

@ -6,6 +6,7 @@
*/
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { v4 as uuidv4 } from 'uuid';
export const journey = new Journey({
@ -17,30 +18,30 @@ export const journey = new Journey({
})
.step('Go to Dashboards Page', async ({ page, kbnUrl }) => {
await page.goto(kbnUrl.get(`/app/dashboards`));
await page.waitForSelector(`[data-test-subj="table-is-ready"]`);
await page.waitForSelector(subj('table-is-ready'));
})
.step('Search dashboards', async ({ page, inputDelays }) => {
await page.type('[data-test-subj="tableListSearchBox"]', 'Web', {
await page.type(subj('tableListSearchBox'), 'Web', {
delay: inputDelays.TYPING,
});
await page.waitForSelector(`[data-test-subj="table-is-ready"]`);
await page.waitForSelector(subj('table-is-ready'));
})
.step('Delete dashboard', async ({ page }) => {
await page.click('[data-test-subj="checkboxSelectRow-edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b"]');
await page.click('[data-test-subj="deleteSelectedItems"]');
await page.click('[data-test-subj="confirmModalConfirmButton"]');
await page.waitForSelector(`[data-test-subj="table-is-ready"]`);
await page.click(subj('checkboxSelectRow-edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b'));
await page.click(subj('deleteSelectedItems'));
await page.click(subj('confirmModalConfirmButton'));
await page.waitForSelector(subj('table-is-ready'));
})
.step('Add dashboard', async ({ page, inputDelays }) => {
await page.click('[data-test-subj="newItemButton"]');
await page.click('[data-test-subj="dashboardSaveMenuItem"]');
await page.type('[data-test-subj="savedObjectTitle"]', `foobar dashboard ${uuidv4()}`, {
await page.click(subj('newItemButton'));
await page.click(subj('dashboardSaveMenuItem'));
await page.type(subj('savedObjectTitle'), `foobar dashboard ${uuidv4()}`, {
delay: inputDelays.TYPING,
});
await page.click('[data-test-subj="confirmSaveSavedObjectButton"]');
await page.locator('[data-test-subj="saveDashboardSuccess"]');
await page.click(subj('confirmSaveSavedObjectButton'));
await page.waitForSelector(subj('saveDashboardSuccess'));
})
.step('Return to dashboard list', async ({ page }) => {
await page.click('[data-test-subj="breadcrumb dashboardListingBreadcrumb first"]');
await page.waitForSelector(`[data-test-subj="table-is-ready"]`);
.step('Return to dashboard list', async ({ kibanaPage, page }) => {
kibanaPage.backToDashboardListing();
await page.waitForSelector(subj('table-is-ready'));
});

View file

@ -6,13 +6,12 @@
*/
import { Journey } from '@kbn/journeys';
import { waitForVisualizations } from '../utils';
export const journey = new Journey({
kbnArchives: ['test/functional/fixtures/kbn_archiver/stress_test'],
esArchives: ['test/functional/fixtures/es_archiver/stress_test'],
}).step('Go to dashboard', async ({ page, kbnUrl, kibanaServer, log }) => {
}).step('Go to dashboard', async ({ page, kbnUrl, kibanaServer, kibanaPage }) => {
await kibanaServer.uiSettings.update({ 'histogram:maxBars': 100 });
await page.goto(kbnUrl.get(`/app/dashboards#/view/92b143a0-2e9c-11ed-b1b6-a504560b392c`));
await waitForVisualizations(page, log, 1);
await kibanaPage.waitForVisualizations(1);
});

View file

@ -7,7 +7,6 @@
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { waitForVisualizations } from '../utils';
export const journey = new Journey({
esArchives: ['x-pack/performance/es_archives/sample_data_ecommerce'],
@ -19,7 +18,7 @@ export const journey = new Journey({
await page.waitForSelector('#dashboardListingHeading');
})
.step('Go to Ecommerce Dashboard', async ({ page, log }) => {
.step('Go to Ecommerce Dashboard', async ({ page, kibanaPage }) => {
await page.click(subj('dashboardListingTitleLink-[eCommerce]-Revenue-Dashboard'));
await waitForVisualizations(page, log, 13);
await kibanaPage.waitForVisualizations(13);
});

View file

@ -18,7 +18,7 @@ export const journey = new Journey({
await page.waitForSelector('#dashboardListingHeading');
})
.step('Go to Ecommerce No Map Dashboard', async ({ page, kbnUrl }) => {
.step('Go to Ecommerce No Map Dashboard', async ({ page }) => {
await page.click(subj('dashboardListingTitleLink-[eCommerce]-Map-Only'));
await page.waitForSelector(
'div[data-title="[eCommerce] Orders by Country"][data-render-complete="true"]'

View file

@ -7,7 +7,6 @@
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { waitForVisualizations } from '../utils';
export const journey = new Journey({
esArchives: ['x-pack/performance/es_archives/sample_data_ecommerce'],
@ -19,7 +18,7 @@ export const journey = new Journey({
await page.waitForSelector('#dashboardListingHeading');
})
.step('Go to Ecommerce Dashboard with Saved Search only', async ({ page, log }) => {
.step('Go to Ecommerce Dashboard with Saved Search only', async ({ page, kibanaPage }) => {
await page.click(subj('dashboardListingTitleLink-[eCommerce]-Saved-Search-Dashboard'));
await waitForVisualizations(page, log, 1);
await kibanaPage.waitForVisualizations(1);
});

View file

@ -7,7 +7,6 @@
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { waitForVisualizations } from '../utils';
export const journey = new Journey({
esArchives: ['x-pack/performance/es_archives/sample_data_ecommerce'],
@ -19,7 +18,7 @@ export const journey = new Journey({
await page.waitForSelector('#dashboardListingHeading');
})
.step('Go to Ecommerce Dashboard with TSVB Gauge only', async ({ page, log }) => {
.step('Go to Ecommerce Dashboard with TSVB Gauge only', async ({ page, kibanaPage }) => {
await page.click(subj('dashboardListingTitleLink-[eCommerce]-TSVB-Gauge-Only-Dashboard'));
await waitForVisualizations(page, log, 1);
await kibanaPage.waitForVisualizations(1);
});

View file

@ -7,7 +7,6 @@
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { waitForVisualizations } from '../utils';
export const journey = new Journey({
esArchives: ['x-pack/performance/es_archives/sample_data_flights'],
@ -19,7 +18,7 @@ export const journey = new Journey({
await page.waitForSelector('#dashboardListingHeading');
})
.step('Go to Flights Dashboard', async ({ page, log }) => {
.step('Go to Flights Dashboard', async ({ page, kibanaPage }) => {
await page.click(subj('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard'));
await waitForVisualizations(page, log, 14);
await kibanaPage.waitForVisualizations(14);
});

View file

@ -7,7 +7,6 @@
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { waitForChrome } from '../utils';
export const journey = new Journey({
skipAutoLogin: true,
@ -34,7 +33,7 @@ export const journey = new Journey({
],
maxDuration: '10m',
},
}).step('Login', async ({ page, kbnUrl, inputDelays, auth }) => {
}).step('Login', async ({ page, kbnUrl, inputDelays, auth, kibanaPage }) => {
await page.goto(kbnUrl.get());
if (auth.isCloud()) {
await page.click(subj('loginCard-basic/cloud-basic'), { delay: inputDelays.MOUSE_CLICK });
@ -44,5 +43,5 @@ export const journey = new Journey({
await page.type(subj('loginPassword'), auth.getPassword(), { delay: inputDelays.TYPING });
await page.click(subj('loginSubmit'), { delay: inputDelays.MOUSE_CLICK });
await waitForChrome(page);
await kibanaPage.waitForHeader();
});

View file

@ -7,15 +7,14 @@
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { waitForChrome } from '../utils';
export const journey = new Journey({
kbnArchives: ['test/functional/fixtures/kbn_archiver/many_fields_data_view'],
esArchives: ['test/functional/fixtures/es_archiver/many_fields'],
})
.step('Go to Discover Page', async ({ page, kbnUrl }) => {
.step('Go to Discover Page', async ({ page, kbnUrl, kibanaPage }) => {
await page.goto(kbnUrl.get(`/app/discover`));
await waitForChrome(page);
await kibanaPage.waitForHeader();
await page.waitForSelector('[data-test-subj="discoverDocTable"][data-render-complete="true"]');
await page.waitForSelector(subj('globalLoadingIndicator-hidden'));
})

View file

@ -7,15 +7,14 @@
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { waitForChrome } from '../utils';
export const journey = new Journey({
kbnArchives: ['test/functional/fixtures/kbn_archiver/many_fields_data_view'],
esArchives: ['test/functional/fixtures/es_archiver/many_fields'],
})
.step('Go to Transforms', async ({ page, kbnUrl }) => {
.step('Go to Transforms', async ({ page, kbnUrl, kibanaPage }) => {
await page.goto(kbnUrl.get(`app/management/data/transform`));
await waitForChrome(page);
await kibanaPage.waitForHeader();
await page.waitForSelector(subj('transformCreateFirstButton'));
await page.waitForSelector(subj('globalLoadingIndicator-hidden'));
})

View file

@ -7,7 +7,6 @@
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { waitForVisualizations } from '../utils';
export const journey = new Journey({
kbnArchives: ['x-pack/performance/kbn_archives/promotion_tracking_dashboard'],
@ -50,6 +49,6 @@ export const journey = new Journey({
await page.click(subj('superDatePickerCommonlyUsed_Last_30 days'));
})
.step('Wait for visualization animations to finish', async ({ page, log }) => {
await waitForVisualizations(page, log, 1);
.step('Wait for visualization animations to finish', async ({ kibanaPage }) => {
await kibanaPage.waitForVisualizations(1);
});

View file

@ -7,7 +7,6 @@
import { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
import { waitForVisualizations } from '../utils';
export const journey = new Journey({
esArchives: ['x-pack/performance/es_archives/sample_data_logs'],
@ -19,7 +18,7 @@ export const journey = new Journey({
await page.waitForSelector('#dashboardListingHeading');
})
.step('Go to Web Logs Dashboard', async ({ page, log }) => {
.step('Go to Web Logs Dashboard', async ({ page, kibanaPage }) => {
await page.click(subj('dashboardListingTitleLink-[Logs]-Web-Traffic'));
await waitForVisualizations(page, log, 11);
await kibanaPage.waitForVisualizations(11);
});

View file

@ -1,32 +0,0 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ToolingLog } from '@kbn/tooling-log';
import { Page } from 'playwright';
export async function waitForChrome(page: Page) {
return page.waitForSelector('.headerGlobalNav', { state: 'attached' });
}
export async function waitForVisualizations(page: Page, log: ToolingLog, visCount: number) {
try {
await page.waitForFunction(function renderCompleted(cnt) {
const visualizations = Array.from(document.querySelectorAll('[data-rendering-count]'));
const allVisLoaded = visualizations.length === cnt;
return allVisLoaded
? visualizations.every((e) => e.getAttribute('data-render-complete') === 'true')
: false;
}, visCount);
} catch (err) {
const loadedVis = await page.$$('[data-rendering-count]');
const renderedVis = await page.$$('[data-rendering-count][data-render-complete="true"]');
log.error(
`'waitForVisualizations' failed: loaded - ${loadedVis.length}, rendered - ${renderedVis.length}, expected - ${visCount}`
);
throw err;
}
}