mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
2fa485a29b
commit
2bb237fc4f
25 changed files with 8740 additions and 183 deletions
|
@ -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"
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
|
|
|
@ -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}`), ''),
|
||||
|
|
Binary file not shown.
|
@ -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
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from '../functional/page_objects';
|
||||
export const pageObjects = {};
|
||||
|
|
|
@ -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';
|
19
x-pack/test/performance/services/index.ts
Normal file
19
x-pack/test/performance/services/index.ts
Normal 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,
|
||||
};
|
33
x-pack/test/performance/services/input_delays.ts
Normal file
33
x-pack/test/performance/services/input_delays.ts
Normal 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;
|
||||
}
|
248
x-pack/test/performance/services/performance.ts
Normal file
248
x-pack/test/performance/services/performance.ts
Normal 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 });
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
72
x-pack/test/performance/tests/playwright/flight_dashboard.ts
Normal file
72
x-pack/test/performance/tests/playwright/flight_dashboard.ts
Normal 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';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
16
x-pack/test/performance/tests/playwright/index.ts
Normal file
16
x-pack/test/performance/tests/playwright/index.ts
Normal 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'));
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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 };
|
||||
};
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue