diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/README.md b/x-pack/solutions/observability/plugins/apm/ui_tests/README.md new file mode 100644 index 0000000000000..68fd9a1fa4127 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/README.md @@ -0,0 +1,23 @@ +## How to run tests + +First start the servers: + +```bash +// ESS +node scripts/scout.js start-server --stateful + +// Serverless +node scripts/scout.js start-server --serverless=[es|oblt|security] +``` + +Then you can run the parallel tests in another terminal: + +```bash +// ESS +npx playwright test --config x-pack/solutions/observability/plugins/apm/ui_tests/parallel.playwright.config.ts --grep @ess + +// Serverless +npx playwright test --config x-pack/solutions/observability/plugins/apm/ui_tests/parallel.playwright.config.ts --grep @svlOblt +``` + +Test results are available in `x-pack/solutions/observability/plugins/apm/ui_tests/output` diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/index.ts b/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/index.ts new file mode 100644 index 0000000000000..5c9e4769c2d0d --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/index.ts @@ -0,0 +1,41 @@ +/* + * 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 type { PageObjects, ScoutTestFixtures, ScoutWorkerFixtures, KibanaUrl } from '@kbn/scout'; +import { test as base, createLazyPageObject } from '@kbn/scout'; +import { ServiceMapPage } from './page_objects/service_map'; +import { ServiceInventoryPage } from './page_objects/service_inventory'; + +export interface ExtendedScoutTestFixtures extends ScoutTestFixtures { + pageObjects: PageObjects & { + serviceMapPage: ServiceMapPage; + serviceInventoryPage: ServiceInventoryPage; + }; +} + +export const test = base.extend({ + pageObjects: async ( + { + pageObjects, + page, + kbnUrl, + }: { + pageObjects: ExtendedScoutTestFixtures['pageObjects']; + page: ExtendedScoutTestFixtures['page']; + kbnUrl: KibanaUrl; + }, + use: (pageObjects: ExtendedScoutTestFixtures['pageObjects']) => Promise + ) => { + const extendedPageObjects = { + ...pageObjects, + serviceMapPage: createLazyPageObject(ServiceMapPage, page, kbnUrl), + serviceInventoryPage: createLazyPageObject(ServiceInventoryPage, page, kbnUrl), + }; + + await use(extendedPageObjects); + }, +}); diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/page_objects/service_inventory.ts b/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/page_objects/service_inventory.ts new file mode 100644 index 0000000000000..fceff0e6710c9 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/page_objects/service_inventory.ts @@ -0,0 +1,24 @@ +/* + * 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 type { KibanaUrl, ScoutPage } from '@kbn/scout'; + +export class ServiceInventoryPage { + constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} + + async waitForPageToLoad() { + await this.page.waitForSelector( + '[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]', + { state: 'visible' } + ); + } + + async gotoDetailedServiceInventoryWithDateSelected(start: string, end: string) { + this.page.goto(`${this.kbnUrl.app('apm')}/services?&rangeFrom=${start}&rangeTo=${end}`); + await this.waitForPageToLoad(); + } +} diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/page_objects/service_map.ts b/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/page_objects/service_map.ts new file mode 100644 index 0000000000000..fc82c51765e60 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/page_objects/service_map.ts @@ -0,0 +1,40 @@ +/* + * 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 type { KibanaUrl, ScoutPage } from '@kbn/scout'; + +export class ServiceMapPage { + constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} + + async waitForPageToLoad() { + await this.page.waitForSelector( + '[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]', + { state: 'visible' } + ); + } + + async gotoWithDateSelected(start: string, end: string) { + this.page.goto(`${this.kbnUrl.app('apm')}/service-map?&rangeFrom=${start}&rangeTo=${end}`); + await this.waitForPageToLoad(); + } + async gotoDetailedServiceMapWithDateSelected(start: string, end: string) { + this.page.goto( + `${this.kbnUrl.app( + 'apm' + )}/services/opbeans-java/service-map?&rangeFrom=${start}&rangeTo=${end}` + ); + await this.waitForPageToLoad(); + } + async getSearchBar() { + await this.page.testSubj.waitForSelector('apmUnifiedSearchBar'); + } + async typeInTheSearchBar() { + await this.getSearchBar(); + await this.page.testSubj.typeWithDelay('apmUnifiedSearchBar', '_id : foo'); + await this.page.getByTestId('querySubmitButton').press('Enter'); + } +} diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/synthtrace/opbeans.ts b/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/synthtrace/opbeans.ts new file mode 100644 index 0000000000000..dcfccfb53460b --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/fixtures/synthtrace/opbeans.ts @@ -0,0 +1,74 @@ +/* + * 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 { apm, timerange } from '@kbn/apm-synthtrace-client'; + +export function opbeans({ from, to }: { from: number; to: number }) { + const range = timerange(from, to); + + const opbeansJava = apm + .service({ + name: 'opbeans-java', + environment: 'production', + agentName: 'java', + }) + .instance('opbeans-java-prod-1') + .podId('opbeans-java-prod-1-pod'); + + const opbeansNode = apm + .service({ + name: 'opbeans-node', + environment: 'production', + agentName: 'nodejs', + }) + .instance('opbeans-node-prod-1'); + + const opbeansRum = apm.browser({ + serviceName: 'opbeans-rum', + environment: 'production', + userAgent: apm.getChromeUserAgentDefaults(), + }); + + return range + .interval('1s') + .rate(1) + .generator((timestamp) => [ + opbeansJava + .transaction({ transactionName: 'GET /api/product' }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + opbeansJava.error({ message: '[MockError] Foo', type: `Exception` }).timestamp(timestamp) + ) + .children( + opbeansJava + .span({ + spanName: 'SELECT * FROM product', + spanType: 'db', + spanSubtype: 'postgresql', + }) + .timestamp(timestamp) + .duration(50) + .failure() + .destination('postgresql') + ), + opbeansNode + .transaction({ transactionName: 'GET /api/product/:id' }) + .timestamp(timestamp) + .duration(500) + .success(), + opbeansNode + .transaction({ + transactionName: 'Worker job', + transactionType: 'Worker', + }) + .timestamp(timestamp) + .duration(1000) + .success(), + opbeansRum.transaction({ transactionName: '/' }).timestamp(timestamp).duration(1000), + ]); +} diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/parallel.playwright.config.ts b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel.playwright.config.ts new file mode 100644 index 0000000000000..70035f4b5a40c --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel.playwright.config.ts @@ -0,0 +1,15 @@ +/* + * 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 { createPlaywrightConfig } from '@kbn/scout'; + +// eslint-disable-next-line import/no-default-export +export default createPlaywrightConfig({ + globalSetup: require.resolve('./parallel_tests/global_setup'), + testDir: './parallel_tests', + workers: 2, +}); diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/global_setup.ts b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/global_setup.ts new file mode 100644 index 0000000000000..81b0bca98df51 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/global_setup.ts @@ -0,0 +1,31 @@ +/* + * 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 { ingestSynthtraceDataHook } from '@kbn/scout'; +import { type FullConfig } from '@playwright/test'; +import { opbeans } from '../fixtures/synthtrace/opbeans'; + +async function globalSetup(config: FullConfig) { + const start = '2021-10-10T00:00:00.000Z'; + const end = '2021-10-10T00:15:00.000Z'; + const data = { + apm: [ + opbeans({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + }), + ], + // TODO add infra and otel fixtures + infra: [], + otel: [], + }; + + return ingestSynthtraceDataHook(config, data); +} + +// eslint-disable-next-line import/no-default-export +export default globalSetup; diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/service_inventory/service_inventory.spec.ts b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/service_inventory/service_inventory.spec.ts new file mode 100644 index 0000000000000..3d7d555006103 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/service_inventory/service_inventory.spec.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { expect } from '@kbn/scout'; +import { test } from '../../fixtures'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; + +test.describe('Service Inventory', { tag: ['@ess', '@svlOblt'] }, () => { + test.beforeEach(async ({ browserAuth, page, pageObjects: { serviceInventoryPage } }) => { + await browserAuth.loginAsViewer(); + await serviceInventoryPage.gotoDetailedServiceInventoryWithDateSelected(start, end); + await page.waitForSelector( + '[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]', + { state: 'visible' } + ); + }); + + test('shows the service inventory', async ({ page, pageObjects: { serviceInventoryPage } }) => { + await serviceInventoryPage.gotoDetailedServiceInventoryWithDateSelected(start, end); + expect(page.url()).toContain('/app/apm/services'); + await expect(page.getByRole('heading', { name: 'Services', level: 1 })).toBeVisible(); + }); + + test('shows a list of services', async ({ page }) => { + await expect(page.getByText('opbeans-node')).toBeVisible(); + await expect(page.getByText('opbeans-java')).toBeVisible(); + await expect(page.getByText('opbeans-rum')).toBeVisible(); + }); + + test('shows a list of environments', async ({ page }) => { + const environmentEntrySelector = page.locator('td:has-text("production")'); + await expect(environmentEntrySelector).toHaveCount(3); + }); + + test('loads the service overview for a service when clicking on it', async ({ page }) => { + await page.getByText('opbeans-node').click(); + expect(page.url()).toContain('/apm/services/opbeans-node/overview'); + await expect(page.getByTestId('apmMainTemplateHeaderServiceName')).toHaveText('opbeans-node'); + }); + + test('shows the correct environment when changing the environment', async ({ page }) => { + await page + .locator('[data-test-subj="environmentFilter"]') + .locator('[data-test-subj="comboBoxSearchInput"]') + .click(); + await expect( + page.getByTestId('comboBoxOptionsList environmentFilter-optionsList') + ).toBeVisible(); + await page + .locator('[data-test-subj="comboBoxOptionsList environmentFilter-optionsList"]') + .locator('button:has-text("production")') + .click(); + await expect(page.getByTestId('comboBoxSearchInput')).toHaveValue('production'); + }); + + test('shows the filtered services when using the service name fast filter', async ({ page }) => { + await expect(page.getByTestId('tableSearchInput')).toBeVisible(); + await expect(page.getByText('opbeans-node')).toBeVisible(); + await expect(page.getByText('opbeans-java')).toBeVisible(); + await expect(page.getByText('opbeans-rum')).toBeVisible(); + await page.getByTestId('tableSearchInput').fill('java'); + await expect(page.getByText('opbeans-node')).toBeHidden(); + await expect(page.getByText('opbeans-java')).toBeVisible(); + await expect(page.getByText('opbeans-rum')).toBeHidden(); + await page.getByTestId('tableSearchInput').clear(); + await expect(page.getByText('opbeans-node')).toBeVisible(); + await expect(page.getByText('opbeans-java')).toBeVisible(); + await expect(page.getByText('opbeans-rum')).toBeVisible(); + }); +}); diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/sevice_map/service_map.spec.ts b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/sevice_map/service_map.spec.ts new file mode 100644 index 0000000000000..589556855afc3 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/sevice_map/service_map.spec.ts @@ -0,0 +1,67 @@ +/* + * 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 { expect } from '@kbn/scout'; +import { test } from '../../fixtures'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; + +test.describe('Service Map', { tag: ['@ess', '@svlOblt'] }, () => { + test.beforeEach(async ({ browserAuth, page, pageObjects: { serviceMapPage } }) => { + await browserAuth.loginAsViewer(); + await serviceMapPage.gotoWithDateSelected(start, end); + await page.waitForSelector( + '[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]', + { state: 'visible' } + ); + }); + test('shows the service map', async ({ page, pageObjects: { serviceMapPage } }) => { + await serviceMapPage.gotoWithDateSelected(start, end); + expect(page.url()).toContain('/app/apm/service-map'); + await page.waitForSelector('[data-test-subj="serviceMap"]'); + await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden(); + await page.getByLabel('Zoom In').click(); + await page.getByTestId('centerServiceMap').click(); + await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden(); + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.getByTestId('serviceMap').click(); + const serviceMapLocator = await page.getByTestId('serviceMap'); + await expect(serviceMapLocator).toHaveScreenshot('service_map.png', { + animations: 'disabled', + maxDiffPixels: 10, + }); + }); + + test('shows a detailed service map', async ({ page, pageObjects: { serviceMapPage } }) => { + await serviceMapPage.gotoDetailedServiceMapWithDateSelected(start, end); + expect(page.url()).toContain('/services/opbeans-java/service-map'); + await page.waitForSelector('[data-test-subj="serviceMap"]'); + await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden(); + await page.getByLabel('Zoom out').click(); + await page.getByTestId('centerServiceMap').click(); + await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden(); + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.getByTestId('serviceMap').click(); + const serviceMapLocator = await page.getByTestId('serviceMap'); + await expect(serviceMapLocator).toHaveScreenshot('detailed_service_map.png', { + animations: 'disabled', + maxDiffPixels: 10, + }); + }); + + test('shows empty state when there is no data', async ({ + page, + pageObjects: { serviceMapPage }, + }) => { + await serviceMapPage.typeInTheSearchBar(); + await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden(); + page.getByText('No services available'); + // search bar is still visible + await expect(page.getByTestId('apmUnifiedSearchBar')).toBeVisible(); + }); +}); diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/sevice_map/service_map.spec.ts-snapshots/detailed-service-map-chromium-darwin.png b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/sevice_map/service_map.spec.ts-snapshots/detailed-service-map-chromium-darwin.png new file mode 100644 index 0000000000000..ef9087c3ff123 Binary files /dev/null and b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/sevice_map/service_map.spec.ts-snapshots/detailed-service-map-chromium-darwin.png differ diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/sevice_map/service_map.spec.ts-snapshots/service-map-chromium-darwin.png b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/sevice_map/service_map.spec.ts-snapshots/service-map-chromium-darwin.png new file mode 100644 index 0000000000000..a5745cbe4f564 Binary files /dev/null and b/x-pack/solutions/observability/plugins/apm/ui_tests/parallel_tests/sevice_map/service_map.spec.ts-snapshots/service-map-chromium-darwin.png differ diff --git a/x-pack/solutions/observability/plugins/apm/ui_tests/tsconfig.json b/x-pack/solutions/observability/plugins/apm/ui_tests/tsconfig.json new file mode 100644 index 0000000000000..95c76f3f661a2 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/ui_tests/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "**/*" + ], + "kbn_references": [ + "@kbn/scout", + "@kbn/apm-synthtrace-client" + ], + "exclude": [ + "target/**/*" + ] +}