Skip to content

Commit

Permalink
[performance] enable journeys on serverless - part 1 (#162902)
Browse files Browse the repository at this point in the history
## 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>
  • Loading branch information
dmlemeshko and kibanamachine committed Aug 2, 2023
1 parent 2fd6af9 commit 4dadcbb
Show file tree
Hide file tree
Showing 23 changed files with 182 additions and 95 deletions.
3 changes: 3 additions & 0 deletions packages/kbn-journeys/journey/journey.ts
Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion packages/kbn-journeys/journey/journey_ftr_config.ts
Expand Up @@ -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';
Expand Down
12 changes: 7 additions & 5 deletions packages/kbn-journeys/journey/journey_ftr_harness.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/kbn-journeys/services/auth.ts
Expand Up @@ -102,4 +102,8 @@ export class Auth {
public isCloud() {
return this.config.get('servers.kibana.hostname') !== 'localhost';
}

public isServerless() {
return !!this.config.get('serverless');
}
}
16 changes: 16 additions & 0 deletions packages/kbn-journeys/services/page/index.ts
@@ -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);
}
75 changes: 75 additions & 0 deletions packages/kbn-journeys/services/page/kibana_page.ts
@@ -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',
});
}
}
22 changes: 22 additions & 0 deletions packages/kbn-journeys/services/page/project_page.ts
@@ -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'));
}
}
1 change: 1 addition & 0 deletions packages/kbn-journeys/tsconfig.json
Expand Up @@ -17,6 +17,7 @@
"@kbn/tooling-log",
"@kbn/repo-info",
"@kbn/std",
"@kbn/test-subj-selector",
],
"exclude": [
"target/**/*",
Expand Down
20 changes: 10 additions & 10 deletions x-pack/performance/journeys/apm_service_inventory.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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'));
});
3 changes: 2 additions & 1 deletion x-pack/performance/journeys/cloud_security_dashboard.ts
Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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'));
});
31 changes: 16 additions & 15 deletions x-pack/performance/journeys/dashboard_listing_page.ts
Expand Up @@ -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({
Expand All @@ -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'));
});
5 changes: 2 additions & 3 deletions x-pack/performance/journeys/data_stress_test_lens.ts
Expand Up @@ -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);
});
5 changes: 2 additions & 3 deletions x-pack/performance/journeys/ecommerce_dashboard.ts
Expand Up @@ -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'],
Expand All @@ -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);
});
Expand Up @@ -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"]'
Expand Down
Expand Up @@ -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'],
Expand All @@ -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);
});
Expand Up @@ -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'],
Expand All @@ -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);
});
5 changes: 2 additions & 3 deletions x-pack/performance/journeys/flight_dashboard.ts
Expand Up @@ -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'],
Expand All @@ -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);
});

0 comments on commit 4dadcbb

Please sign in to comment.