Add Playwright Service and New User Journeys for Performance tests (#124259)

add playwright service and single-user journeys for performance tests

-  Modifies @kbn/test package to call Playwright Service without constructor
- Adds Playwright service to performance tests
- Adds following performance user journeys:
  - Ecommerce Dashboard
  - Flights Dashboard & edit visualization
  - Weblogs Dashboard
  - Promotion Tracking Dashboard

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Baturalp Gurdin 2022-03-08 19:56:00 +01:00 committed by GitHub
parent 2fa485a29b
commit 2bb237fc4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 8740 additions and 183 deletions

View file

@ -13,29 +13,43 @@ node scripts/es snapshot&
esPid=$!
export TEST_PERFORMANCE_PHASE=WARMUP
export TEST_ES_URL=http://elastic:changeme@localhost:9200
export TEST_ES_DISABLE_STARTUP=true
export ELASTIC_APM_ACTIVE=false
sleep 120
cd "$XPACK_DIR"
# warmup round 1
checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Phase: WARMUP)" \
node scripts/functional_tests \
--debug --bail \
--kibana-install-dir "$KIBANA_BUILD_LOCATION" \
--config "test/performance/config.playwright.ts";
jobId=$(npx uuid)
export TEST_JOB_ID="$jobId"
export TEST_PERFORMANCE_PHASE=TEST
export ELASTIC_APM_ACTIVE=true
journeys=("login" "ecommerce_dashboard" "flight_dashboard" "web_logs_dashboard" "promotion_tracking_dashboard")
checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Phase: TEST)" \
node scripts/functional_tests \
--debug --bail \
--kibana-install-dir "$KIBANA_BUILD_LOCATION" \
--config "test/performance/config.playwright.ts";
for i in "${journeys[@]}"; do
echo "JOURNEY[${i}] is running"
export TEST_PERFORMANCE_PHASE=WARMUP
export ELASTIC_APM_ACTIVE=false
export JOURNEY_NAME="${i}"
checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: WARMUP)" \
node scripts/functional_tests \
--config test/performance/config.playwright.ts \
--include "test/performance/tests/playwright/${i}.ts" \
--kibana-install-dir "$KIBANA_BUILD_LOCATION" \
--debug \
--bail
export TEST_PERFORMANCE_PHASE=TEST
export ELASTIC_APM_ACTIVE=true
checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: TEST)" \
node scripts/functional_tests \
--config test/performance/config.playwright.ts \
--include "test/performance/tests/playwright/${i}.ts" \
--kibana-install-dir "$KIBANA_BUILD_LOCATION" \
--debug \
--bail
done
kill "$esPid"

View file

@ -155,8 +155,9 @@ export class FunctionalTestRunner {
readProviderSpec(type, providers).map((p) => ({
...p,
fn: skip.includes(p.name)
? (...args: unknown[]) => {
const result = p.fn(...args);
? (ctx: any) => {
const result = ProviderCollection.callProviderFn(p.fn, ctx);
if ('then' in result) {
throw new Error(
`Provider [${p.name}] returns a promise so it can't loaded during test analysis`

View file

@ -7,7 +7,14 @@
*/
export { FunctionalTestRunner } from './functional_test_runner';
export { readConfigFile, Config, EsVersion, Lifecycle, LifecyclePhase } from './lib';
export {
readConfigFile,
Config,
createAsyncInstance,
EsVersion,
Lifecycle,
LifecyclePhase,
} from './lib';
export type { ScreenshotRecord } from './lib';
export { runFtrCli } from './cli';
export * from './lib/docker_servers';

View file

@ -9,7 +9,7 @@
export { Lifecycle } from './lifecycle';
export { LifecyclePhase } from './lifecycle_phase';
export { readConfigFile, Config } from './config';
export { readProviderSpec, ProviderCollection } from './providers';
export * from './providers';
// @internal
export { runTests, setupMocha } from './mocha';
export * from './test_metadata';

View file

@ -8,4 +8,5 @@
export { ProviderCollection } from './provider_collection';
export { readProviderSpec } from './read_provider_spec';
export { createAsyncInstance } from './async_instance';
export type { Provider } from './read_provider_spec';

View file

@ -15,6 +15,15 @@ import { createVerboseInstance } from './verbose_instance';
import { GenericFtrService } from '../../public_types';
export class ProviderCollection {
static callProviderFn(providerFn: any, ctx: any) {
if (providerFn.prototype instanceof GenericFtrService) {
const Constructor = providerFn as any as new (ctx: any) => any;
return new Constructor(ctx);
}
return providerFn(ctx);
}
private readonly instances = new Map();
constructor(private readonly log: ToolingLog, private readonly providers: Providers) {}
@ -59,19 +68,12 @@ export class ProviderCollection {
}
public invokeProviderFn(provider: (args: any) => any) {
const ctx = {
return ProviderCollection.callProviderFn(provider, {
getService: this.getService,
hasService: this.hasService,
getPageObject: this.getPageObject,
getPageObjects: this.getPageObjects,
};
if (provider.prototype instanceof GenericFtrService) {
const Constructor = provider as any as new (ctx: any) => any;
return new Constructor(ctx);
}
return provider(ctx);
});
}
private findProvider(type: string, name: string) {

View file

@ -30,6 +30,7 @@ exports[`should render popover when appLinks is not empty 1`] = `
"id": 0,
"items": Array [
Object {
"data-test-subj": "viewSampleDataSetecommerce-dashboard",
"href": "root/app/dashboards#/view/722b74f0-b882-11e8-a6d9-e546fe2bba5f",
"icon": <EuiIcon
size="m"

View file

@ -69,6 +69,9 @@ export class SampleDataViewDataButton extends React.Component {
onClick: createAppNavigationHandler(path),
};
});
/** @typedef {import('@elastic/eui').EuiContextMenuProps['panels']} EuiContextMenuPanels */
/** @type {EuiContextMenuPanels} */
const panels = [
{
id: 0,
@ -80,6 +83,7 @@ export class SampleDataViewDataButton extends React.Component {
icon: <EuiIcon type="dashboardApp" size="m" />,
href: prefixedDashboardPath,
onClick: createAppNavigationHandler(dashboardPath),
'data-test-subj': `viewSampleDataSet${this.props.id}-dashboard`,
},
...additionalItems,
],

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import uuid from 'uuid';
import { FtrConfigProviderContext } from '@kbn/test';
import { services } from './services';
@ -14,15 +14,19 @@ import { pageObjects } from './page_objects';
const APM_SERVER_URL = 'https://2fad4006bf784bb8a54e52f4a5862609.apm.us-west1.gcp.cloud.es.io:443';
const APM_PUBLIC_TOKEN = 'Q5q5rWQEw6tKeirBpw';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
export default async function ({ readConfigFile, log }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(require.resolve('../functional/config'));
const testFiles = [require.resolve('./tests/playwright/home.ts')];
const testFiles = [require.resolve('./tests/playwright')];
const testJobId = process.env.TEST_JOB_ID ?? uuid();
log.info(`👷 JOB ID ${testJobId}👷`);
return {
testFiles,
services,
pageObjects,
servicesRequiredForTestAnalysis: ['performance'],
servers: functionalConfig.get('servers'),
esTestCluster: functionalConfig.get('esTestCluster'),
apps: functionalConfig.get('apps'),
@ -32,6 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [...functionalConfig.get('kbnTestServer.serverArgs')],
env: {
ELASTIC_APM_ACTIVE: process.env.ELASTIC_APM_ACTIVE,
ELASTIC_APM_CONTEXT_PROPAGATION_ONLY: 'false',
@ -41,7 +46,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
ELASTIC_APM_SECRET_TOKEN: APM_PUBLIC_TOKEN,
ELASTIC_APM_GLOBAL_LABELS: Object.entries({
ftrConfig: `x-pack/test/performance/tests/config.playwright`,
performancePhase: process.env.PERF_TEST_PHASE,
performancePhase: process.env.TEST_PERFORMANCE_PHASE,
journeyName: process.env.JOURNEY_NAME,
testJobId,
})
.filter(([, v]) => !!v)
.reduce((acc, [k, v]) => (acc ? `${acc},${k}=${v}` : `${k}=${v}`), ''),

View file

@ -0,0 +1,219 @@
{
"type": "index",
"value": {
"aliases": {
},
"index": "kibana_sample_data_ecommerce",
"mappings": {
"properties": {
"category": {
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"currency": {
"type": "keyword"
},
"customer_birth_date": {
"type": "date"
},
"customer_first_name": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"customer_full_name": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"customer_gender": {
"type": "keyword"
},
"customer_id": {
"type": "keyword"
},
"customer_last_name": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"customer_phone": {
"type": "keyword"
},
"day_of_week": {
"type": "keyword"
},
"day_of_week_i": {
"type": "integer"
},
"email": {
"type": "keyword"
},
"event": {
"properties": {
"dataset": {
"type": "keyword"
}
}
},
"geoip": {
"properties": {
"city_name": {
"type": "keyword"
},
"continent_name": {
"type": "keyword"
},
"country_iso_code": {
"type": "keyword"
},
"location": {
"type": "geo_point"
},
"region_name": {
"type": "keyword"
}
}
},
"manufacturer": {
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"order_date": {
"type": "date"
},
"order_id": {
"type": "keyword"
},
"products": {
"properties": {
"_id": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"base_price": {
"type": "half_float"
},
"base_unit_price": {
"type": "half_float"
},
"category": {
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"created_on": {
"type": "date"
},
"discount_amount": {
"type": "half_float"
},
"discount_percentage": {
"type": "half_float"
},
"manufacturer": {
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"min_price": {
"type": "half_float"
},
"price": {
"type": "half_float"
},
"product_id": {
"type": "long"
},
"product_name": {
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword"
}
},
"type": "text"
},
"quantity": {
"type": "integer"
},
"sku": {
"type": "keyword"
},
"tax_amount": {
"type": "half_float"
},
"taxful_price": {
"type": "half_float"
},
"taxless_price": {
"type": "half_float"
},
"unit_discount_amount": {
"type": "half_float"
}
}
},
"sku": {
"type": "keyword"
},
"taxful_total_price": {
"type": "half_float"
},
"taxless_total_price": {
"type": "half_float"
},
"total_quantity": {
"type": "integer"
},
"total_unique_products": {
"type": "integer"
},
"type": {
"type": "keyword"
},
"user": {
"type": "keyword"
}
}
},
"settings": {
"index": {
"auto_expand_replicas": "0-1",
"number_of_replicas": "0",
"number_of_shards": "1"
}
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export * from '../functional/page_objects';
export const pageObjects = {};

View file

@ -1,8 +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.
*/
export * from '../functional/services';

View file

@ -0,0 +1,19 @@
/*
* 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 { services as functionalServices } from '../../functional/services';
import { PerformanceTestingService } from './performance';
import { InputDelaysProvider } from './input_delays';
export const services = {
es: functionalServices.es,
kibanaServer: functionalServices.kibanaServer,
esArchiver: functionalServices.esArchiver,
retry: functionalServices.retry,
performance: PerformanceTestingService,
inputDelays: InputDelaysProvider,
};

View file

@ -0,0 +1,33 @@
/*
* 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.
*/
interface InputDelays {
TYPING: number;
MOUSE_CLICK: number;
}
const PROFILES: Record<string, InputDelays> = {
user: {
TYPING: 500,
MOUSE_CLICK: 1000,
},
asap: {
TYPING: 5,
MOUSE_CLICK: 5,
},
};
export function InputDelaysProvider(): InputDelays {
const profile = PROFILES[process.env.INPUT_DELAY_PROFILE ?? 'user'];
if (!profile) {
throw new Error(
`invalid INPUT_DELAY_PROFILE value, expected one of (${Object.keys(PROFILES).join(', ')})`
);
}
return profile;
}

View file

@ -0,0 +1,248 @@
/*
* 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.
*/
/* eslint-disable no-console */
import Url from 'url';
import { inspect } from 'util';
import apm, { Span, Transaction } from 'elastic-apm-node';
import { setTimeout } from 'timers/promises';
import playwright, { ChromiumBrowser, Page, BrowserContext } from 'playwright';
import { FtrService, FtrProviderContext } from '../ftr_provider_context';
type StorageState = Awaited<ReturnType<BrowserContext['storageState']>>;
apm.start({
secretToken: 'Q5q5rWQEw6tKeirBpw',
serverUrl: 'https://2fad4006bf784bb8a54e52f4a5862609.apm.us-west1.gcp.cloud.es.io:443',
serviceName: 'functional test runner',
});
interface StepCtx {
page: Page;
}
type StepFn = (ctx: StepCtx) => Promise<void>;
type Steps = Array<{ name: string; fn: StepFn }>;
export class PerformanceTestingService extends FtrService {
private readonly config = this.ctx.getService('config');
private readonly lifecycle = this.ctx.getService('lifecycle');
private readonly inputDelays = this.ctx.getService('inputDelays');
private browser: ChromiumBrowser | undefined;
private storageState: StorageState | undefined;
private currentSpanStack: Array<Span | null> = [];
private currentTransaction: Transaction | undefined | null;
constructor(ctx: FtrProviderContext) {
super(ctx);
this.lifecycle.beforeTests.add(async () => {
await this.withTransaction('Journey setup', async () => {
await this.getStorageState();
});
});
this.lifecycle.cleanup.add(async () => {
apm.flush();
await setTimeout(5000);
await this.browser?.close();
});
}
private async withTransaction<T>(name: string, block: () => Promise<T>) {
try {
if (this.currentTransaction !== undefined) {
throw new Error(
`Transaction already started, make sure you end transaction ${this.currentTransaction?.name}`
);
}
this.currentTransaction = apm.startTransaction(name, 'performance');
const result = await block();
if (this.currentTransaction === undefined) {
throw new Error(`No transaction started`);
}
this.currentTransaction?.end('success');
this.currentTransaction = undefined;
return result;
} catch (e) {
if (this.currentTransaction === undefined) {
throw new Error(`No transaction started`);
}
this.currentTransaction?.end('failure');
this.currentTransaction = undefined;
throw e;
}
}
private async withSpan<T>(name: string, type: string | undefined, block: () => Promise<T>) {
try {
this.currentSpanStack.unshift(apm.startSpan(name, type ?? null));
const result = await block();
if (this.currentSpanStack.length === 0) {
throw new Error(`No Span started`);
}
const span = this.currentSpanStack.shift();
span?.setOutcome('success');
span?.end();
return result;
} catch (e) {
if (this.currentSpanStack.length === 0) {
throw new Error(`No Span started`);
}
const span = this.currentSpanStack.shift();
span?.setOutcome('failure');
span?.end();
throw e;
}
}
private getCurrentTraceparent() {
return (this.currentSpanStack.length ? this.currentSpanStack[0] : this.currentTransaction)
?.traceparent;
}
private async getStorageState() {
if (this.storageState) {
return this.storageState;
}
await this.withSpan('initial login', undefined, async () => {
const kibanaUrl = Url.format({
protocol: this.config.get('servers.kibana.protocol'),
hostname: this.config.get('servers.kibana.hostname'),
port: this.config.get('servers.kibana.port'),
});
const browser = await this.getBrowserInstance();
const context = await browser.newContext();
const page = await context.newPage();
await this.interceptBrowserRequests(page);
await page.goto(`${kibanaUrl}`);
const usernameLocator = page.locator('[data-test-subj=loginUsername]');
const passwordLocator = page.locator('[data-test-subj=loginPassword]');
const submitButtonLocator = page.locator('[data-test-subj=loginSubmit]');
await usernameLocator?.type('elastic', { delay: this.inputDelays.TYPING });
await passwordLocator?.type('changeme', { delay: this.inputDelays.TYPING });
await submitButtonLocator?.click({ delay: this.inputDelays.MOUSE_CLICK });
await page.waitForSelector('#headerUserMenu');
this.storageState = await page.context().storageState();
await page.close();
await context.close();
});
return this.storageState;
}
private async getBrowserInstance() {
if (this.browser) {
return this.browser;
}
return await this.withSpan('browser creation', 'setup', async () => {
const headless = !!(process.env.TEST_BROWSER_HEADLESS || process.env.CI);
this.browser = await playwright.chromium.launch({ headless, timeout: 60000 });
return this.browser;
});
}
private async sendCDPCommands(context: BrowserContext, page: Page) {
const client = await context.newCDPSession(page);
await client.send('Network.clearBrowserCache');
await client.send('Network.setCacheDisabled', { cacheDisabled: true });
await client.send('Network.emulateNetworkConditions', {
latency: 100,
downloadThroughput: 750_000,
uploadThroughput: 750_000,
offline: false,
});
return client;
}
private async interceptBrowserRequests(page: Page) {
await page.route('**', async (route, request) => {
const headers = await request.allHeaders();
const traceparent = this.getCurrentTraceparent();
if (traceparent && request.isNavigationRequest()) {
await route.continue({ headers: { traceparent, ...headers } });
} else {
await route.continue();
}
});
}
public makePage(journeyName: string) {
const steps: Steps = [];
it(journeyName, async () => {
await this.withTransaction(`Journey ${journeyName}`, async () => {
const browser = await this.getBrowserInstance();
const context = await browser.newContext({
viewport: { width: 1600, height: 1200 },
storageState: await this.getStorageState(),
});
const page = await context.newPage();
page.on('console', (message) => {
(async () => {
try {
const args = await Promise.all(
message.args().map(async (handle) => handle.jsonValue())
);
const { url, lineNumber, columnNumber } = message.location();
const location = `${url},${lineNumber},${columnNumber}`;
const text = args.length
? args.map((arg) => (typeof arg === 'string' ? arg : inspect(arg))).join(' ')
: message.text();
console.log(`[console.${message.type()}]`, text);
console.log(' ', location);
} catch (e) {
console.error('Failed to evaluate console.log line', e);
}
})();
});
const client = await this.sendCDPCommands(context, page);
await this.interceptBrowserRequests(page);
try {
for (const step of steps) {
await this.withSpan(`step: ${step.name}`, 'step', async () => {
try {
await step.fn({ page });
} catch (e) {
const error = new Error(`Step [${step.name}] failed: ${e.message}`);
error.stack = e.stack;
throw error;
}
});
}
} finally {
if (page) {
await client.detach();
await page.close();
await context.close();
}
}
});
});
return {
step: (name: string, fn: StepFn) => {
steps.push({ name, fn });
},
};
}
}

View file

@ -1,53 +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 { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObject }: FtrProviderContext) {
const retry = getService('retry');
const es = getService('es');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const common = getPageObject('common');
const dashboard = getPageObject('dashboard');
const reporting = getPageObject('reporting');
describe('Reporting Dashboard', () => {
before(async () => {
await kibanaServer.importExport.load(
'x-pack/test/performance/kbn_archives/reporting_dashboard'
);
await esArchiver.loadIfNeeded('x-pack/test/performance/es_archives/reporting_dashboard');
});
after(async () => {
await kibanaServer.importExport.unload(
'x-pack/test/performance/kbn_archives/reporting_dashboard'
);
await esArchiver.unload('x-pack/test/performance/es_archives/reporting_dashboard');
await es.deleteByQuery({
index: '.reporting-*',
refresh: true,
body: { query: { match_all: {} } },
});
});
it('downloaded PDF has OK status', async function () {
this.timeout(180000);
await common.navigateToApp('dashboards');
await retry.waitFor('dashboard landing page', async () => {
return await dashboard.onDashboardLandingPage();
});
await dashboard.loadSavedDashboard('dashboard');
await reporting.openPdfReportingPanel();
await reporting.clickGenerateReportButton();
await reporting.getReportURL(60000);
});
});
}

View file

@ -0,0 +1,56 @@
/*
* 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 Url from 'url';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ecommerceDashboard({ getService }: FtrProviderContext) {
describe('ecommerce_dashboard', () => {
const config = getService('config');
const performance = getService('performance');
const logger = getService('log');
const { step } = performance.makePage('ecommerce_dashboard');
step('Go to Sample Data Page', async ({ page }) => {
const kibanaUrl = Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'),
});
await page.goto(`${kibanaUrl}/app/home#/tutorial_directory/sampleData`);
await page.waitForSelector('text="More ways to add data"');
});
step('Add Ecommerce Sample Data', async ({ page }) => {
const removeButton = page.locator('[data-test-subj=removeSampleDataSetecommerce]');
try {
await removeButton.click({ timeout: 1_000 });
} catch (e) {
logger.info('Ecommerce data does not exist');
}
const addDataButton = page.locator('[data-test-subj=addSampleDataSetecommerce]');
if (addDataButton) {
await addDataButton.click();
}
});
step('Go to Ecommerce Dashboard', async ({ page }) => {
await page.click('[data-test-subj=launchSampleDataSetecommerce]');
await page.click('[data-test-subj=viewSampleDataSetecommerce-dashboard]');
await page.waitForFunction(() => {
const visualizations = Array.from(document.querySelectorAll('[data-rendering-count]'));
const visualizationElementsLoaded = visualizations.length > 0;
const visualizationAnimationsFinished = visualizations.every(
(e) => e.getAttribute('data-render-complete') === 'true'
);
return visualizationElementsLoaded && visualizationAnimationsFinished;
});
});
});
}

View file

@ -0,0 +1,72 @@
/*
* 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 Url from 'url';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function flightDashboard({ getService }: FtrProviderContext) {
describe('flights_dashboard', () => {
const config = getService('config');
const performance = getService('performance');
const logger = getService('log');
const { step } = performance.makePage('flights_dashboard');
step('Go to Sample Data Page', async ({ page }) => {
const kibanaUrl = Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'),
});
await page.goto(`${kibanaUrl}/app/home#/tutorial_directory/sampleData`);
await page.waitForSelector('[data-test-subj=sampleDataSetCardflights]');
});
step('Add Flights Sample Data', async ({ page }) => {
const removeButton = page.locator('[data-test-subj=removeSampleDataSetflights]');
try {
await removeButton.click({ timeout: 1_000 });
} catch (e) {
logger.info('Flights data does not exist');
}
const addDataButton = page.locator('[data-test-subj=addSampleDataSetflights]');
if (addDataButton) {
await addDataButton.click();
}
});
step('Go to Flights Dashboard', async ({ page }) => {
await page.click('[data-test-subj=launchSampleDataSetflights]');
await page.click('[data-test-subj=viewSampleDataSetflights-dashboard]');
await page.waitForFunction(() => {
const visualizations = Array.from(document.querySelectorAll('[data-rendering-count]'));
const visualizationElementsLoaded = visualizations.length > 0;
const visualizationAnimationsFinished = visualizations.every(
(e) => e.getAttribute('data-render-complete') === 'true'
);
return visualizationElementsLoaded && visualizationAnimationsFinished;
});
});
// embeddablePanelHeading-[Flights]AirportConnections(HoverOverAirport)
step('Go to Airport Connections Visualizations Edit', async ({ page }) => {
await page.click('[data-test-subj="dashboardEditMode"]');
const flightsPanelHeadingSelector = `[data-test-subj="embeddablePanelHeading-[Flights]AirportConnections(HoverOverAirport)"]`;
const panelToggleMenuIconSelector = `[data-test-subj="embeddablePanelToggleMenuIcon"]`;
await page.click(`${flightsPanelHeadingSelector} ${panelToggleMenuIconSelector}`);
await page.click('[data-test-subj="embeddablePanelAction-editPanel"]');
await page.waitForFunction(() => {
const visualization = document.querySelector('[data-rendering-count]');
return visualization && visualization?.getAttribute('data-render-complete') === 'true';
});
});
});
}

View file

@ -1,55 +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 Url from 'url';
import { ChromiumBrowser, Page } from 'playwright';
import testSetup from './setup';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
describe('perf_login_and_home', () => {
const config = getService('config');
const kibanaUrl = Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'),
});
let page: Page | null = null;
let browser: ChromiumBrowser | null = null;
before(async () => {
const context = await testSetup();
page = context.page;
browser = context.browser;
});
after(async () => {
await browser?.close();
});
it('Go to Kibana login page', async () => {
await page?.goto(`${kibanaUrl}`);
});
it('Login to Kibana', async () => {
const usernameLocator = page?.locator('[data-test-subj=loginUsername]');
const passwordLocator = page?.locator('[data-test-subj=loginPassword]');
const submitButtonLocator = page?.locator('[data-test-subj=loginSubmit]');
await usernameLocator?.type('elastic', { delay: 500 });
await passwordLocator?.type('changeme', { delay: 500 });
await submitButtonLocator?.click({ delay: 1000 });
});
it('Dismiss Welcome Screen', async () => {
await page?.waitForLoadState();
const skipButtonLocator = page?.locator('[data-test-subj=skipWelcomeScreen]');
await skipButtonLocator?.click({ delay: 1000 });
await page?.waitForLoadState('networkidle');
});
});
}

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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Performance tests', () => {
loadTestFile(require.resolve('./ecommerce_dashboard'));
loadTestFile(require.resolve('./flight_dashboard'));
loadTestFile(require.resolve('./web_logs_dashboard'));
loadTestFile(require.resolve('./promotion_tracking_dashboard'));
});
}

View file

@ -0,0 +1,73 @@
/*
* 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 Url from 'url';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function promotionTrackingDashboard({ getService }: FtrProviderContext) {
describe('promotion_tracking_dashboard', () => {
const config = getService('config');
const performance = getService('performance');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const { step } = performance.makePage('promotion_tracking_dashboard');
before(async () => {
await kibanaServer.importExport.load(
'x-pack/test/performance/kbn_archives/promotion_tracking_dashboard'
);
await esArchiver.loadIfNeeded('x-pack/test/performance/es_archives/ecommerce_sample_data');
});
after(async () => {
await kibanaServer.importExport.unload(
'x-pack/test/performance/kbn_archives/promotion_tracking_dashboard'
);
await esArchiver.unload('x-pack/test/performance/es_archives/ecommerce_sample_data');
});
step('Go to Dashboards Page', async ({ page }) => {
const kibanaUrl = Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'),
});
await page.goto(`${kibanaUrl}/app/dashboards`);
await page.waitForSelector('#dashboardListingHeading');
});
step('Go to Promotion Tracking Dashboard', async ({ page }) => {
const promotionDashboardButton = page.locator(
'[data-test-subj="dashboardListingTitleLink-Promotion-Dashboard"]'
);
await promotionDashboardButton.click();
});
step('Change time range', async ({ page }) => {
const beginningTimeRangeButton = page.locator(
'[data-test-subj="superDatePickerToggleQuickMenuButton"]'
);
await beginningTimeRangeButton.click();
const lastYearButton = page.locator(
'[data-test-subj="superDatePickerCommonlyUsed_Last_30 days"]'
);
await lastYearButton.click();
});
step('Wait for visualization animations to finish', async ({ page }) => {
await page.waitForFunction(() => {
const visualizations = Array.from(document.querySelectorAll('[data-rendering-count]'));
const visualizationElementsLoaded = visualizations.length > 0;
const visualizationAnimationsFinished = visualizations.every(
(e) => e.getAttribute('data-render-complete') === 'true'
);
return visualizationElementsLoaded && visualizationAnimationsFinished;
});
});
});
}

View file

@ -1,34 +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 playwright, { ChromiumBrowser, Page } from 'playwright';
interface ITestSetup {
browser: ChromiumBrowser;
page: Page;
}
const headless = process.env.TEST_BROWSER_HEADLESS === '1';
export default async (): Promise<ITestSetup> => {
const browser = await playwright.chromium.launch({ headless });
const page = await browser.newPage();
const client = await page.context().newCDPSession(page);
await client.send('Network.clearBrowserCache');
await client.send('Network.setCacheDisabled', { cacheDisabled: true });
await client.send('Network.emulateNetworkConditions', {
latency: 100,
downloadThroughput: 750_000,
uploadThroughput: 750_000,
offline: false,
});
await page.route('**', (route) => route.continue());
return { browser, page };
};

View file

@ -0,0 +1,56 @@
/*
* 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 Url from 'url';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function weblogDashboard({ getService }: FtrProviderContext) {
describe('weblogs_dashboard', () => {
const config = getService('config');
const performance = getService('performance');
const logger = getService('log');
const { step } = performance.makePage('weblogs_dashboard');
step('Go to Sample Data Page', async ({ page }) => {
const kibanaUrl = Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'),
});
await page.goto(`${kibanaUrl}/app/home#/tutorial_directory/sampleData`);
await page.waitForSelector('text="More ways to add data"');
});
step('Add Web Logs Sample Data', async ({ page }) => {
const removeButton = page.locator('[data-test-subj=removeSampleDataSetlogs]');
try {
await removeButton.click({ timeout: 1_000 });
} catch (e) {
logger.info('Weblogs data does not exist');
}
const addDataButton = page.locator('[data-test-subj=addSampleDataSetlogs]');
if (addDataButton) {
await addDataButton.click();
}
});
step('Go to Web Logs Dashboard', async ({ page }) => {
await page.click('[data-test-subj=launchSampleDataSetlogs]');
await page.click('[data-test-subj=viewSampleDataSetlogs-dashboard]');
await page.waitForFunction(() => {
const visualizations = Array.from(document.querySelectorAll('[data-rendering-count]'));
const visualizationElementsLoaded = visualizations.length > 0;
const visualizationAnimationsFinished = visualizations.every(
(e) => e.getAttribute('data-render-complete') === 'true'
);
return visualizationElementsLoaded && visualizationAnimationsFinished;
});
});
});
}