diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/transactions_overview/transactions_overview.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/transactions_overview/transactions_overview.cy.ts deleted file mode 100644 index beebae076c3cb..0000000000000 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/transactions_overview/transactions_overview.cy.ts +++ /dev/null @@ -1,130 +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 { apm, timerange } from '@kbn/apm-synthtrace-client'; -import { synthtrace } from '../../../synthtrace'; -import { checkA11y } from '../../support/commands'; - -const start = '2021-10-10T00:00:00.000Z'; -const end = '2021-10-10T00:15:00.000Z'; - -const goServiceTransactionsHref = url.format({ - pathname: '/app/apm/services/service-go/transactions', - query: { rangeFrom: start, rangeTo: end }, -}); - -function generateData() { - const transactionNames = ['GET', 'PUT', 'DELETE', 'UPDATE'].flatMap((method) => - [ - '/users', - '/products', - '/orders', - '/customers', - '/profile', - '/categories', - '/invoices', - '/payments', - '/cart', - '/reviews', - ].map((resource) => `${method} ${resource}`) - ); - - const nodeService = apm - .service({ - name: `service-node`, - environment: 'production', - agentName: 'nodejs', - }) - .instance('opbeans-node-prod-1'); - - const goService = apm - .service({ - name: `service-go`, - environment: 'production', - agentName: 'go', - }) - .instance('opbeans-node-prod-1'); - - const from = new Date(start).getTime(); - const to = new Date(end).getTime(); - const range = timerange(from, to); - - return range - .interval('2m') - .rate(1) - .generator((timestamp) => - transactionNames.flatMap((transactionName) => [ - goService - .transaction({ - transactionName, - transactionType: 'request', - }) - .timestamp(timestamp) - .duration(500) - .success(), - ...['request', 'Worker'].map((type) => - nodeService - .transaction({ - transactionName, - transactionType: type, - }) - .timestamp(timestamp) - .duration(500) - .success() - ), - ]) - ); -} -describe('Transactions Overview', () => { - before(() => { - synthtrace.index(generateData()); - }); - - after(() => { - synthtrace.clean(); - }); - - beforeEach(() => { - cy.loginAsViewerUser(); - }); - - it('has no detectable a11y violations on load', () => { - cy.visitKibana(goServiceTransactionsHref); - cy.get('a:contains(Transactions)').should('have.attr', 'aria-selected', 'true'); - // set skipFailures to true to not fail the test when there are accessibility failures - checkA11y({ skipFailures: true }); - }); - - it('persists transaction type selected when navigating to Overview tab', () => { - const nodeServiceTransactionsHref = url.format({ - pathname: '/app/apm/services/service-node/transactions', - query: { rangeFrom: start, rangeTo: end }, - }); - cy.visitKibana(nodeServiceTransactionsHref); - cy.getByTestSubj('headerFilterTransactionType').should('have.value', 'request'); - cy.getByTestSubj('headerFilterTransactionType').select('Worker'); - cy.getByTestSubj('headerFilterTransactionType').should('have.value', 'Worker'); - cy.get('a[href*="/app/apm/services/service-node/overview"]').click(); - cy.getByTestSubj('headerFilterTransactionType').should('have.value', 'Worker'); - }); - - it('includes the correct transactionNames in the detailed statistics request', () => { - cy.visitKibana(goServiceTransactionsHref); - - cy.intercept('GET', '**/internal/apm/services/*/transactions/groups/detailed_statistics?*').as( - 'detailedStats' - ); - - cy.wait('@detailedStats').then((detailedInterception) => { - const decodedUrl = decodeURIComponent(detailedInterception.request.url); - expect(decodedUrl).to.include( - 'transactionNames=["DELETE /cart","DELETE /categories","DELETE /customers","DELETE /invoices","DELETE /orders","DELETE /payments","DELETE /products","DELETE /profile","DELETE /reviews","DELETE /users"]' - ); - }); - }); -}); diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/constants.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/constants.ts index 1fa163aa6c167..1ff7722def056 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/constants.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/constants.ts @@ -9,6 +9,7 @@ import type { KibanaRole } from '@kbn/scout-oblt'; export const OPBEANS_START_DATE = '2021-10-10T00:00:00.000Z'; export const OPBEANS_END_DATE = '2021-10-10T00:15:00.000Z'; +export const BIGGER_TIMEOUT = 45000; // APM-specific role definitions matching authentication.ts export const APM_ROLES = { diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/index.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/index.ts index 6e81b9a99d38d..7226f0a04f515 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/index.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/index.ts @@ -23,7 +23,9 @@ import { AgentConfigurationsPage } from './page_objects/agent_configurations'; import { AgentExplorerPage } from './page_objects/agent_explorer'; import { AgentKeysPage } from './page_objects/agent_keys'; import { AnomalyDetectionPage } from './page_objects/anomaly_detection'; +import { TransactionsOverviewPage } from './page_objects/transactions_overview'; import { APM_ROLES } from './constants'; +import { TransactionDetailsPage } from './page_objects/transaction_details'; export interface ApmBrowserAuthFixture extends BrowserAuthFixture { loginAsApmAllPrivilegesWithoutWriteSettings: () => Promise; @@ -42,6 +44,8 @@ export interface ExtendedScoutTestFixtures extends ObltTestFixtures { agentExplorerPage: AgentExplorerPage; agentKeysPage: AgentKeysPage; anomalyDetectionPage: AnomalyDetectionPage; + transactionsOverviewPage: TransactionsOverviewPage; + transactionDetailsPage: TransactionDetailsPage; }; browserAuth: ApmBrowserAuthFixture; } @@ -70,6 +74,8 @@ export const test = base.extend({ agentExplorerPage: createLazyPageObject(AgentExplorerPage, page, kbnUrl), agentKeysPage: createLazyPageObject(AgentKeysPage, page, kbnUrl), anomalyDetectionPage: createLazyPageObject(AnomalyDetectionPage, page, kbnUrl), + transactionsOverviewPage: createLazyPageObject(TransactionsOverviewPage, page, kbnUrl), + transactionDetailsPage: createLazyPageObject(TransactionDetailsPage, page, kbnUrl), serviceGroupsPage: createLazyPageObject(ServiceGroupsPage, page, kbnUrl), }; diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_helpers.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_helpers.ts new file mode 100644 index 0000000000000..ed065181355ef --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_helpers.ts @@ -0,0 +1,27 @@ +/* + * 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 { ScoutPage } from '@kbn/scout-oblt'; +import { BIGGER_TIMEOUT } from './constants'; + +/** + * Waits for the APM settings header link to be visible. + * This is commonly used to ensure the APM page has fully loaded. + */ +export async function waitForApmSettingsHeaderLink(page: ScoutPage): Promise { + await page + .getByTestId('apmSettingsHeaderLink') + .waitFor({ state: 'visible', timeout: BIGGER_TIMEOUT }); +} + +/** + * Waits for the APM main container to be visible. + * This is commonly used to ensure the APM page has fully loaded. + */ +export async function waitForApmMainContainer(page: ScoutPage): Promise { + await page.testSubj.waitForSelector('apmMainContainer', { timeout: BIGGER_TIMEOUT }); +} diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_configurations.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_configurations.ts index 7e48799ba3741..bc774e677c2a3 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_configurations.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_configurations.ts @@ -14,13 +14,14 @@ import type { KibanaUrl, ScoutPage } from '@kbn/scout-oblt'; import { EuiComboBoxWrapper, EuiFieldTextWrapper } from '@kbn/scout-oblt'; +import { waitForApmMainContainer } from '../page_helpers'; export class AgentConfigurationsPage { constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} async goto() { await this.page.goto(`${this.kbnUrl.app('apm')}/settings/agent-configuration`); - await this.page.waitForLoadingIndicatorHidden(); + await waitForApmMainContainer(this.page); // Wait for the page content to load this.page.getByRole('heading', { name: 'Settings', level: 1 }); diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_explorer.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_explorer.ts index d8c1825133cf8..811d81957ebc1 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_explorer.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_explorer.ts @@ -6,6 +6,7 @@ */ import type { KibanaUrl, ScoutPage } from '@kbn/scout-oblt'; +import { waitForApmMainContainer } from '../page_helpers'; export class AgentExplorerPage { constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} @@ -16,8 +17,7 @@ export class AgentExplorerPage { 'apm' )}/settings/agent-explorer?kuery=&agentLanguage=&serviceName=&comparisonEnabled=true&environment=ENVIRONMENT_ALL` ); - await this.page.waitForLoadingIndicatorHidden(); - this.page.getByRole('heading', { name: 'Settings', level: 1 }); + await waitForApmMainContainer(this.page); return this.page; } diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_keys.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_keys.ts index ce2b362ea788e..8b8774d2f5690 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_keys.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/agent_keys.ts @@ -6,13 +6,14 @@ */ import type { KibanaUrl, ScoutPage } from '@kbn/scout-oblt'; +import { waitForApmSettingsHeaderLink } from '../page_helpers'; export class AgentKeysPage { constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} async goto() { await this.page.goto(`${this.kbnUrl.app('apm')}/settings/agent-keys`); - await this.page.waitForLoadingIndicatorHidden(); + await waitForApmSettingsHeaderLink(this.page); this.page.getByRole('heading', { name: 'Settings', level: 1 }); return this.page; diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/anomaly_detection.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/anomaly_detection.ts index 84b3a7b532fc4..aa070e91d9f4d 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/anomaly_detection.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/anomaly_detection.ts @@ -7,13 +7,15 @@ import type { KibanaUrl, ScoutPage } from '@kbn/scout-oblt'; import { EuiComboBoxWrapper } from '@kbn/scout-oblt'; +import { BIGGER_TIMEOUT } from '../constants'; +import { waitForApmMainContainer } from '../page_helpers'; export class AnomalyDetectionPage { constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} async goto() { await this.page.goto(`${this.kbnUrl.app('apm')}/settings/anomaly-detection`); - await this.page.waitForLoadingIndicatorHidden(); + await waitForApmMainContainer(this.page); // Wait for the page content to load this.page.getByRole('heading', { name: 'Settings', level: 1 }); @@ -41,6 +43,7 @@ export class AnomalyDetectionPage { async selectEnvironment(environmentName: string) { const environmentComboBox = new EuiComboBoxWrapper(this.page, { locator: '.euiComboBox' }); await environmentComboBox.setCustomMultiOption(environmentName); + await this.page.keyboard.press('Escape'); } async clickCreateJobsButton() { @@ -55,4 +58,15 @@ export class AnomalyDetectionPage { this.page.getByText('Anomaly detection jobs created'); } + + async deleteMlJob() { + const manageJobsButton = this.page.testSubj.locator('apmMLManageJobsTextLink'); + await manageJobsButton.waitFor({ state: 'visible', timeout: BIGGER_TIMEOUT }); + await manageJobsButton.click(); + const allActionsButton = this.page.getByLabel('All actions, row 1'); + await allActionsButton.click(); + await this.page.testSubj.locator('mlActionButtonDeleteJob').click(); + await this.page.testSubj.locator('mlDeleteJobConfirmModalButton').click(); + await expect(this.page.getByText('deleted successfully')).toBeVisible(); + } } diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/custom_links.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/custom_links.ts index 31eda0bd80b80..278284da3fad5 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/custom_links.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/custom_links.ts @@ -7,6 +7,7 @@ import type { KibanaUrl, Locator, ScoutPage } from '@kbn/scout-oblt'; import { expect } from '@kbn/scout-oblt/ui'; +import { waitForApmSettingsHeaderLink } from '../page_helpers'; export class CustomLinksPage { public saveButton: Locator; @@ -16,7 +17,7 @@ export class CustomLinksPage { async goto() { await this.page.goto(`${this.kbnUrl.app('apm')}/settings/custom-links`); - return this.page.waitForLoadingIndicatorHidden(); + return await waitForApmSettingsHeaderLink(this.page); } async getCreateCustomLinkButton() { diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/general_settings.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/general_settings.ts index f5c4aaeadec9f..1213953036fe5 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/general_settings.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/general_settings.ts @@ -6,13 +6,14 @@ */ import type { KibanaUrl, ScoutPage } from '@kbn/scout-oblt'; +import { waitForApmSettingsHeaderLink } from '../page_helpers'; export class GeneralSettingsPage { constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} async goto() { await this.page.goto(`${this.kbnUrl.app('apm')}/settings/general-settings`); - return this.page.waitForLoadingIndicatorHidden(); + return await waitForApmSettingsHeaderLink(this.page); } async getInspectEsQueriesButton() { diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/indices.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/indices.ts index deb09610dbadb..ded4686fe5aa2 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/indices.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/indices.ts @@ -6,13 +6,14 @@ */ import type { KibanaUrl, ScoutPage } from '@kbn/scout-oblt'; +import { waitForApmSettingsHeaderLink } from '../page_helpers'; export class IndicesPage { constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} async goto() { await this.page.goto(`${this.kbnUrl.app('apm')}/settings/apm-indices`); - return this.page.waitForLoadingIndicatorHidden(); + return await waitForApmSettingsHeaderLink(this.page); } async getErrorIndexInput() { diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_groups.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_groups.ts index 0440c9a83a245..6a2d2a65a5a14 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_groups.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_groups.ts @@ -7,6 +7,7 @@ import type { KibanaUrl, ScoutPage } from '@kbn/scout-oblt'; import { expect } from '@kbn/scout-oblt/ui'; +import { waitForApmSettingsHeaderLink } from '../page_helpers'; export class ServiceGroupsPage { constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} @@ -15,7 +16,7 @@ export class ServiceGroupsPage { await this.page.goto( `${this.kbnUrl.app('apm')}/service-groups?&rangeFrom=${start}&rangeTo=${end}` ); - return this.page.waitForLoadingIndicatorHidden(); + return await waitForApmSettingsHeaderLink(this.page); } async typeInTheSearchBar(text: string) { diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_inventory.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_inventory.ts index df8dd98f866a2..a5c65e2ac3499 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_inventory.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_inventory.ts @@ -6,12 +6,13 @@ */ import type { KibanaUrl, ScoutPage } from '@kbn/scout-oblt'; +import { BIGGER_TIMEOUT } from '../constants'; export class ServiceInventoryPage { constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} async gotoDetailedServiceInventoryWithDateSelected(start: string, end: string) { await this.page.goto(`${this.kbnUrl.app('apm')}/services?&rangeFrom=${start}&rangeTo=${end}`); - return this.page.waitForLoadingIndicatorHidden(); + await this.page.testSubj.waitForSelector('apmUnifiedSearchBar', { timeout: BIGGER_TIMEOUT }); } } diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_map.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_map.ts index 025d16285e1cd..cdc69375c5b7b 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_map.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_map.ts @@ -7,6 +7,7 @@ import type { KibanaUrl, Locator, ScoutPage } from '@kbn/scout-oblt'; import { expect } from '@kbn/scout-oblt/ui'; +import { waitForApmSettingsHeaderLink } from '../page_helpers'; export class ServiceMapPage { public serviceMap: Locator; @@ -27,7 +28,7 @@ export class ServiceMapPage { await this.page.goto( `${this.kbnUrl.app('apm')}/service-map?&rangeFrom=${start}&rangeTo=${end}` ); - return this.page.waitForLoadingIndicatorHidden(); + return await waitForApmSettingsHeaderLink(this.page); } async gotoDetailedServiceMapWithDateSelected(start: string, end: string) { @@ -36,7 +37,7 @@ export class ServiceMapPage { 'apm' )}/services/opbeans-java/service-map?&rangeFrom=${start}&rangeTo=${end}` ); - return this.page.waitForLoadingIndicatorHidden(); + return await waitForApmSettingsHeaderLink(this.page); } async getSearchBar() { diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/transaction_details.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/transaction_details.ts new file mode 100644 index 0000000000000..9a5de1f6256a9 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/transaction_details.ts @@ -0,0 +1,46 @@ +/* + * 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-oblt'; +import { waitForApmSettingsHeaderLink } from '../page_helpers'; + +export class TransactionDetailsPage { + constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} + + async goToTransactionDetails(params: { + serviceName: string; + transactionName: string; + start: string; + end: string; + }) { + const { serviceName, transactionName, start, end } = params; + + const urlServiceName = encodeURIComponent(serviceName); + + await this.page.goto( + `${this.kbnUrl.app('apm')}/services/${urlServiceName}/transactions/view?${new URLSearchParams( + { + transactionName, + rangeFrom: start, + rangeTo: end, + } + )}` + ); + await waitForApmSettingsHeaderLink(this.page); + } + + async reload() { + await this.page.reload(); + await waitForApmSettingsHeaderLink(this.page); + } + + async fillApmUnifiedSearchBar(query: string) { + const searchBar = this.page.getByTestId('apmUnifiedSearchBar'); + await searchBar.fill(query); + await searchBar.press('Enter'); + } +} diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/transactions_overview.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/transactions_overview.ts new file mode 100644 index 0000000000000..5da413b221f46 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/transactions_overview.ts @@ -0,0 +1,34 @@ +/* + * 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-oblt'; +import { BIGGER_TIMEOUT } from '../constants'; + +export class TransactionsOverviewPage { + constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {} + + async goto(serviceName: string, rangeFrom: string, rangeTo: string) { + await this.page.goto( + `${this.kbnUrl.app( + 'apm' + )}/services/${serviceName}/transactions?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`, + { timeout: BIGGER_TIMEOUT } + ); + await this.page + .getByTestId('apmMainTemplateHeaderServiceName') + .waitFor({ timeout: BIGGER_TIMEOUT }); + } + + getTransactionTypeFilter() { + return this.page.testSubj.locator('headerFilterTransactionType'); + } + + async selectTransactionType(type: string) { + const filter = this.getTransactionTypeFilter(); + await filter.selectOption(type); + } +} diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/synthtrace/opbeans.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/synthtrace/opbeans.ts index 55d0c090844a1..7a9b988aa312b 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/synthtrace/opbeans.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/synthtrace/opbeans.ts @@ -7,6 +7,21 @@ import type { ApmFields, SynthtraceGenerator } from '@kbn/apm-synthtrace-client'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; +const SERVICE_GO_TRANSACTION_NAMES = ['GET', 'PUT', 'DELETE', 'UPDATE'].flatMap((method) => + [ + '/cart', + '/categories', + '/customers', + '/invoices', + '/orders', + '/payments', + '/products', + '/profile', + '/reviews', + '/users', + ].map((resource) => `${method} ${resource}`) +); + export function opbeans({ from, to, @@ -39,6 +54,22 @@ export function opbeans({ userAgent: apm.getChromeUserAgentDefaults(), }); + const opbeansGo = apm + .service({ + name: 'service-go', + environment: 'production', + agentName: 'go', + }) + .instance('service-go-prod-1'); + + const serviceNode = apm + .service({ + name: 'service-node', + environment: 'production', + agentName: 'nodejs', + }) + .instance('service-node-prod-1'); + return range .interval('1s') .rate(1) @@ -77,5 +108,31 @@ export function opbeans({ .duration(1000) .success(), opbeansRum.transaction({ transactionName: '/' }).timestamp(timestamp).duration(1000), + ...SERVICE_GO_TRANSACTION_NAMES.map((transactionName) => + opbeansGo + .transaction({ + transactionName, + transactionType: 'request', + }) + .timestamp(timestamp) + .duration(500) + .success() + ), + serviceNode + .transaction({ + transactionName: 'GET /api/users', + transactionType: 'request', + }) + .timestamp(timestamp) + .duration(500) + .success(), + serviceNode + .transaction({ + transactionName: 'Background job', + transactionType: 'Worker', + }) + .timestamp(timestamp) + .duration(500) + .success(), ]); } diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_groups/service_groups.spec.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_groups/service_groups.spec.ts index 0728a392a88a2..3444af3e4312a 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_groups/service_groups.spec.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_groups/service_groups.spec.ts @@ -6,6 +6,7 @@ */ import { expect } from '@kbn/scout-oblt/ui'; import { test, testData } from '../../fixtures'; +import { waitForApmSettingsHeaderLink } from '../../fixtures/page_helpers'; test.describe('Service Groups', { tag: ['@ess', '@svlOblt'] }, () => { test.beforeEach(async ({ browserAuth, pageObjects: { serviceGroupsPage } }) => { @@ -20,6 +21,8 @@ test.describe('Service Groups', { tag: ['@ess', '@svlOblt'] }, () => { page, pageObjects: { serviceGroupsPage }, }) => { + const GO_SERVICE_GROUP_NAME = 'go services'; + await test.step('shows no service groups initially', async () => { // If there are no service groups, the page shows this heading await expect( @@ -28,7 +31,7 @@ test.describe('Service Groups', { tag: ['@ess', '@svlOblt'] }, () => { }); await test.step('creates a service group', async () => { - await serviceGroupsPage.createNewServiceGroup('go services'); + await serviceGroupsPage.createNewServiceGroup(GO_SERVICE_GROUP_NAME); // open the service picker and filter by agent name await serviceGroupsPage.typeInTheSearchBar('agent.name:"go"'); @@ -36,19 +39,28 @@ test.describe('Service Groups', { tag: ['@ess', '@svlOblt'] }, () => { // verify expected synthetic services are listed and save await serviceGroupsPage.expectByText(['synth-go-1', 'synth-go-2']); await page.getByText('Save group').click(); + + // Make sure the toast is visible and contains the correct text and then close it + await expect(page.getByTestId('euiToastHeader')).toBeVisible(); + await expect( + page.getByTestId('euiToastHeader').getByText(`Created "${GO_SERVICE_GROUP_NAME}" group`) + ).toBeVisible(); + await page.getByTestId('toastCloseButton').click(); + // wait for UI to reflect the created group await expect(page.getByText('1 group')).toBeVisible(); }); await test.step('shows created group in the list', async () => { const card = page.getByTestId('serviceGroupCard'); - await expect(card).toContainText('go services'); + await expect(card).toContainText(GO_SERVICE_GROUP_NAME); await expect(card).toContainText('2 services'); }); await test.step('opens service list when clicking on service group card', async () => { await page.getByTestId('serviceGroupCard').click(); - await serviceGroupsPage.expectByText(['go services', 'synth-go-1', 'synth-go-2']); + await expect(page.getByTestId('apmEditButtonEditGroupButton')).toBeVisible(); + await serviceGroupsPage.expectByText(['synth-go-1', 'synth-go-2']); }); await test.step('deletes the service group', async () => { @@ -57,7 +69,7 @@ test.describe('Service Groups', { tag: ['@ess', '@svlOblt'] }, () => { await page.getByTestId('apmDeleteGroupButton').click(); // after deletion there should be no service groups - await page.waitForLoadingIndicatorHidden(); + await waitForApmSettingsHeaderLink(page); await expect( page.getByRole('heading', { name: 'No service groups', level: 2 }) ).toBeVisible(); diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_inventory/service_inventory.spec.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_inventory/service_inventory.spec.ts index d93447633ed9f..d33e93c02de50 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_inventory/service_inventory.spec.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_inventory/service_inventory.spec.ts @@ -32,7 +32,7 @@ test.describe('Service inventory', { tag: ['@ess', '@svlOblt'] }, () => { await test.step('shows a list of environments', async () => { const environmentEntrySelector = page.locator('td:has-text("production")'); - await expect(environmentEntrySelector).toHaveCount(3); + await expect(environmentEntrySelector).toHaveCount(5); }); }); diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/settings/indices.spec.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/settings/indices.spec.ts index aa74d98a46110..7244a29d42eae 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/settings/indices.spec.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/settings/indices.spec.ts @@ -7,6 +7,7 @@ import { expect } from '@kbn/scout-oblt/ui'; import { test } from '../../fixtures'; +import { waitForApmSettingsHeaderLink } from '../../fixtures/page_helpers'; test.describe('Indices', { tag: ['@ess'] }, () => { test('Viewer should not be able to modify settings', async ({ @@ -41,7 +42,7 @@ test.describe('Indices', { tag: ['@ess'] }, () => { await expect(applyButton).toBeEnabled(); await indicesPage.clickApplyChanges(); - await page.waitForLoadingIndicatorHidden(); + await waitForApmSettingsHeaderLink(page); await expect(await indicesPage.getErrorIndexInput()).toHaveValue(newErrorIndex); }); diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/transactions_overview/transactions_overview.spec.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/transactions_overview/transactions_overview.spec.ts new file mode 100644 index 0000000000000..1c8722f862784 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/transactions_overview/transactions_overview.spec.ts @@ -0,0 +1,60 @@ +/* + * 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-oblt/ui'; +import { test, testData } from '../../fixtures'; +import { waitForApmSettingsHeaderLink } from '../../fixtures/page_helpers'; + +test.describe('Transactions Overview', { tag: ['@ess', '@svlOblt'] }, () => { + test.beforeEach(async ({ browserAuth }) => { + await browserAuth.loginAsViewer(); + }); + + test('Viewer: Page has no detectable a11y violations on load', async ({ + page, + pageObjects: { transactionsOverviewPage }, + }) => { + await transactionsOverviewPage.goto( + 'service-go', + testData.OPBEANS_START_DATE, + testData.OPBEANS_END_DATE + ); + + // Verify Transactions tab is selected (same as original Cypress check) + await expect(page.getByTestId('transactionsTab')).toHaveAttribute('aria-selected', 'true'); + + // Run accessibility check scoped to the Kibana app wrapper (same as Cypress checkA11y) + const { violations } = await page.checkA11y({ include: ['.kbnAppWrapper'] }); + expect(violations).toStrictEqual([]); + }); + + test('Viewer: Persists transaction type selected when navigating to Overview tab', async ({ + page, + pageObjects: { transactionsOverviewPage }, + }) => { + await transactionsOverviewPage.goto( + 'service-node', + testData.OPBEANS_START_DATE, + testData.OPBEANS_END_DATE + ); + + // Verify default transaction type is 'request' + const transactionTypeFilter = transactionsOverviewPage.getTransactionTypeFilter(); + await expect(transactionTypeFilter).toHaveValue('request'); + + // Change to 'Worker' type + await transactionsOverviewPage.selectTransactionType('Worker'); + await expect(transactionTypeFilter).toHaveValue('Worker'); + + // Navigate to Overview tab + await page.getByTestId('overviewTab').click(); + await waitForApmSettingsHeaderLink(page); + + // Verify transaction type is still 'Worker' + await expect(transactionTypeFilter).toHaveValue('Worker'); + }); +}); diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/apm/transactions/index.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/apm/transactions/index.ts index 9b94a1c6679a0..decf0202e6daf 100644 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/apm/transactions/index.ts +++ b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/apm/transactions/index.ts @@ -16,6 +16,7 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('./transactions_groups_alerts.spec.ts')); loadTestFile(require.resolve('./transactions_groups_detailed_statistics.spec.ts')); loadTestFile(require.resolve('./transactions_groups_main_statistics.spec.ts')); + loadTestFile(require.resolve('./transactions_groups_order.spec.ts')); loadTestFile(require.resolve('./trace_samples.spec.ts')); }); } diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/apm/transactions/transactions_groups_order.spec.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/apm/transactions/transactions_groups_order.spec.ts new file mode 100644 index 0000000000000..dfaa94894d810 --- /dev/null +++ b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/apm/transactions/transactions_groups_order.spec.ts @@ -0,0 +1,157 @@ +/* + * 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/expect'; +import { Readable } from 'stream'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; + +interface TransactionsGroupsMainStatistics { + transactionGroups: Array<{ name: string; transactionType: string }>; + maxCountExceeded: boolean; + transactionOverflowCount: number; + hasActiveAlerts: boolean; +} + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'service-go'; + const start = new Date('2021-10-10T00:00:00.000Z').getTime(); + const end = new Date('2021-10-10T00:15:00.000Z').getTime() - 1; + + // Expected transaction names in order + const EXPECTED_TRANSACTION_NAMES = [ + 'DELETE /cart', + 'DELETE /categories', + 'DELETE /customers', + 'DELETE /invoices', + 'DELETE /orders', + 'DELETE /payments', + 'DELETE /products', + 'DELETE /profile', + 'DELETE /reviews', + 'DELETE /users', + ]; + + async function fetchTransactionGroups() { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + useDurationSummary: false, + kuery: '', + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + }, + }, + }); + expect(response.status).to.be(200); + return response.body as TransactionsGroupsMainStatistics; + } + + describe('Transaction groups order', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + const instance = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance-a'); + + const events = timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return EXPECTED_TRANSACTION_NAMES.map((transactionName) => { + return instance + .transaction({ transactionName, transactionType: 'request' }) + .timestamp(timestamp) + .duration(1000) + .success(); + }); + }); + + const unserialized = Array.from(events); + await apmSynthtraceEsClient.index(Readable.from(unserialized)); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns transaction names in the correct order', async () => { + const response = await fetchTransactionGroups(); + const transactionNames = response.transactionGroups.map((group) => group.name); + + // Verify we have at least the expected number of transactions + expect(transactionNames.length >= EXPECTED_TRANSACTION_NAMES.length).to.be(true); + + // Verify the order of the first 10 transactions matches expected order + const firstTenTransactions = transactionNames.slice(0, EXPECTED_TRANSACTION_NAMES.length); + expect(firstTenTransactions).to.eql(EXPECTED_TRANSACTION_NAMES); + + // Verify each transaction has the correct position (index) in the array + EXPECTED_TRANSACTION_NAMES.forEach((expectedName, index) => { + const actualIndex = transactionNames.indexOf(expectedName); + expect(actualIndex).to.be(index); + }); + }); + + it('includes the correct transactionNames in the detailed statistics request', async () => { + const mainStatistics = await fetchTransactionGroups(); + const transactionNames = mainStatistics.transactionGroups + .slice(0, EXPECTED_TRANSACTION_NAMES.length) + .map((group) => group.name); + + // Verify we have the expected transaction names + expect(transactionNames).to.eql(EXPECTED_TRANSACTION_NAMES); + + // Now fetch detailed statistics with these transaction names + const detailedStatisticsResponse = await apmApiClient.readUser({ + endpoint: + 'GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + transactionNames: JSON.stringify(transactionNames), + bucketSizeInSeconds: 60, + useDurationSummary: false, + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + }, + }, + }); + + expect(detailedStatisticsResponse.status).to.be(200); + + // Verify detailed statistics contains data for all expected transaction names + const detailedStats = detailedStatisticsResponse.body; + if (detailedStats.currentPeriod) { + const statsTransactionNames = Object.keys(detailedStats.currentPeriod); + + expect(statsTransactionNames).to.eql(EXPECTED_TRANSACTION_NAMES); + } + }); + }); +}