From 960baa1d150d5542176c030b502377684aebac9d Mon Sep 17 00:00:00 2001 From: neptunian Date: Thu, 5 Dec 2024 13:48:54 -0500 Subject: [PATCH 1/5] add functional tests for data streams dropdown --- .../common/data_usage/intercept_request.ts | 68 +++++++++++++++++ .../test_suites/common/data_usage/main.ts | 73 ++++++++++++++++++- 2 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 x-pack/test_serverless/functional/test_suites/common/data_usage/intercept_request.ts diff --git a/x-pack/test_serverless/functional/test_suites/common/data_usage/intercept_request.ts b/x-pack/test_serverless/functional/test_suites/common/data_usage/intercept_request.ts new file mode 100644 index 0000000000000..eb703def4864e --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/data_usage/intercept_request.ts @@ -0,0 +1,68 @@ +/* + * 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 { WebDriver } from 'selenium-webdriver'; + +export interface ResponseFactory { + fail: (reason?: string) => [string, Record]; + fulfill: (responseOptions: Record) => [string, Record]; +} + +export async function interceptRequest( + driver: WebDriver, + pattern: string, + onIntercept: (responseFactory: ResponseFactory) => [string, Record], + cb: () => Promise +) { + const connection = await driver.createCDPConnection('page'); + + return new Promise((resolve, reject) => { + connection._wsConnection.on('message', async (data: Buffer) => { + const parsed = JSON.parse(data.toString()); + + if (parsed.method === 'Fetch.requestPaused') { + await new Promise((innerResolve) => + connection.execute( + ...onIntercept({ + fail: () => [ + 'Fetch.failRequest', + { requestId: parsed.params.requestId, errorReason: 'Failed' }, + ], + fulfill: (responseOptions: any) => [ + 'Fetch.fulfillRequest', + { + requestId: parsed.params.requestId, + ...responseOptions, + }, + ], + }), + innerResolve + ) + ); + + await new Promise((innerResolve) => connection.execute('Fetch.disable', {}, innerResolve)); + resolve(); + } + }); + + new Promise((innerResolve) => + connection.execute( + 'Fetch.enable', + { + patterns: [{ urlPattern: pattern }], + }, + innerResolve + ) + ) + .then(() => { + return cb(); + }) + .catch((err: Error) => { + reject(err); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts b/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts index 0b59557229cd4..7195d1d870ad2 100644 --- a/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts +++ b/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts @@ -4,14 +4,29 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; +import { interceptRequest } from './intercept_request'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['svlCommonPage', 'svlManagementPage', 'common']); const testSubjects = getService('testSubjects'); const retry = getService('retry'); - + const driver = getService('__webdriver__'); + const dataStreamsMockResponse = [ + { + name: 'metrics-system.cpu-default', + storageSizeBytes: 6197, + }, + { + name: 'metrics-system.core.total.pct-default', + storageSizeBytes: 5197, + }, + { + name: 'logs-nginx.access-default', + storageSizeBytes: 1938, + }, + ]; describe('Main page', function () { this.tags(['skipMKI']); before(async () => { @@ -21,13 +36,65 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return await testSubjects.exists('cards-navigation-page'); }); await pageObjects.svlManagementPage.assertDataUsageManagementCardExists(); - await pageObjects.svlManagementPage.clickDataUsageManagementCard(); + + await interceptRequest( + driver.driver, + '*data_streams*', + (responseFactory) => { + return responseFactory.fulfill({ + responseCode: 200, + responseHeaders: [{ name: 'Content-Type', value: 'application/json' }], + body: Buffer.from(JSON.stringify(dataStreamsMockResponse)).toString('base64'), + }); + }, + async () => { + // await pageObjects.common.navigateToApp('management/data/data_usage'); + await pageObjects.svlManagementPage.clickDataUsageManagementCard(); + } + ); }); it('renders data usage page', async () => { await retry.waitFor('page to be visible', async () => { + await new Promise((resolve) => setTimeout(resolve, 10000)); return await testSubjects.exists('DataUsagePage'); }); }); + it('shows 3 data streams in the filter dropdown', async () => { + // Click the dropdown button to show the options + await testSubjects.click('data-usage-metrics-filter-dataStreams-popoverButton'); + + // Wait for the dropdown options to appear + await retry.waitFor('data streams filter options to appear', async () => { + const options = await testSubjects.findAll('dataStreams-filter-option'); + return options.length === 3; // Wait until exactly 3 options are available + }); + + // Retrieve all the filter options + const options = await testSubjects.findAll('dataStreams-filter-option'); + + // Assert that exactly 3 elements are present + expect(options.length).to.eql(3); + + // Assert that each option is checked + for (const option of options) { + const ariaChecked = await option.getAttribute('aria-checked'); + expect(ariaChecked).to.be('true'); + } + + // Locate the filter button using its data-test-subj + const filterButton = await testSubjects.find( + 'data-usage-metrics-filter-dataStreams-popoverButton' + ); + + // Find the badge element within the button (using its CSS class) + const notificationBadge = await filterButton.findByCssSelector('.euiNotificationBadge'); + + // Retrieve the text content of the badge + const activeFiltersCount = await notificationBadge.getVisibleText(); + + // Assert the badge displays the expected count + expect(activeFiltersCount).to.be('3'); + }); }); }; From 3587b1002ff4c0fb30cf379edbd20a3bf5295d28 Mon Sep 17 00:00:00 2001 From: neptunian Date: Fri, 6 Dec 2024 15:09:56 -0500 Subject: [PATCH 2/5] more tests --- .../public/app/components/chart_panel.tsx | 2 +- .../public/app/components/legend_action.tsx | 2 + .../common/data_usage/mock_data.ts | 16 +++ .../common/data_usage/tests/metrics.ts | 2 +- .../test_suites/common/data_usage/main.ts | 98 +++++++++++++++++-- 5 files changed, 109 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx index 208b1e576c0d7..9a7700a5de828 100644 --- a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx +++ b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx @@ -91,7 +91,7 @@ export const ChartPanel: React.FC = ({ return ( - +
{chartKeyToTitleMap[metricType as ChartKey] || metricType}
diff --git a/x-pack/plugins/data_usage/public/app/components/legend_action.tsx b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx index b748b77163245..6d2df9d78f070 100644 --- a/x-pack/plugins/data_usage/public/app/components/legend_action.tsx +++ b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx @@ -52,6 +52,7 @@ export const LegendAction: React.FC = React.memo( return ( @@ -59,6 +60,7 @@ export const LegendAction: React.FC = React.memo( iconType="boxesHorizontal" aria-label={UX_LABELS.dataQualityPopup.open} onClick={() => togglePopover(uniqueStreamName)} + data-test-subj="legendActionButton" /> diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_usage/mock_data.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_usage/mock_data.ts index 7bd4b7f3e7a22..e0f6c5d43e788 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/data_usage/mock_data.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_usage/mock_data.ts @@ -15,6 +15,14 @@ export const mockAutoOpsResponse = { [1726862130000, 14657904], ], }, + { + name: 'metrics-system.core.total.pct-default', + error: null, + data: [ + [1726858530000, 13756849], + [1726862130000, 14657904], + ], + }, { name: 'logs-nginx.access-default', error: null, @@ -33,6 +41,14 @@ export const mockAutoOpsResponse = { [1726862130000, 13956423], ], }, + { + name: 'metrics-system.core.total.pct-default', + error: null, + data: [ + [1726858530000, 13756849], + [1726862130000, 14657904], + ], + }, { name: 'logs-nginx.access-default', error: null, diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_usage/tests/metrics.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_usage/tests/metrics.ts index 5460b750a5b21..6a0774ea2e8f4 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/data_usage/tests/metrics.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_usage/tests/metrics.ts @@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { const mockAutoopsApiService = setupMockServer(); describe('Metrics', function () { let mockApiServer: http.Server; - // due to the plugin depending on yml config (xpack.dataUsage.enabled), we cannot test in MKI until it is on by default + // MKI has a different config in the QA environment and will ignore the mock service this.tags(['skipMKI']); before(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts b/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts index 7195d1d870ad2..e70e8ce3a8ce8 100644 --- a/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts +++ b/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts @@ -5,14 +5,20 @@ * 2.0. */ import expect from '@kbn/expect'; +import http from 'http'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { interceptRequest } from './intercept_request'; +import { setupMockServer } from '../../../../api_integration/test_suites/common/data_usage/mock_api'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['svlCommonPage', 'svlManagementPage', 'common']); const testSubjects = getService('testSubjects'); const retry = getService('retry'); const driver = getService('__webdriver__'); + const mockAutoopsApiService = setupMockServer(); + const es = getService('es'); + let mockApiServer: http.Server; + const dataStreamsMockResponse = [ { name: 'metrics-system.cpu-default', @@ -30,6 +36,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Main page', function () { this.tags(['skipMKI']); before(async () => { + // create test data streams from the mock data streams response + // so the metrics api can verify they exist + for (const { name } of dataStreamsMockResponse) { + await es.indices.putIndexTemplate({ + name, + body: { + index_patterns: [name], + data_stream: {}, + priority: 200, + }, + }); + await es.indices.createDataStream({ name }); + } await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); await retry.waitFor('page to be visible', async () => { @@ -37,6 +56,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); await pageObjects.svlManagementPage.assertDataUsageManagementCardExists(); + // mock external API request to autoops + mockApiServer = mockAutoopsApiService.listen(9000); + + // intercept the data_streams request to bypass waiting for the metering api to aggregate a response + // otherwise storage sizes get filtered out if they are 0 await interceptRequest( driver.driver, '*data_streams*', @@ -48,15 +72,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }, async () => { - // await pageObjects.common.navigateToApp('management/data/data_usage'); await pageObjects.svlManagementPage.clickDataUsageManagementCard(); } ); }); + after(async () => { + mockApiServer.close(); + for (const { name } of dataStreamsMockResponse) { + await es.indices.deleteDataStream({ name }); + } + }); it('renders data usage page', async () => { await retry.waitFor('page to be visible', async () => { - await new Promise((resolve) => setTimeout(resolve, 10000)); return await testSubjects.exists('DataUsagePage'); }); }); @@ -64,13 +92,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Click the dropdown button to show the options await testSubjects.click('data-usage-metrics-filter-dataStreams-popoverButton'); - // Wait for the dropdown options to appear - await retry.waitFor('data streams filter options to appear', async () => { - const options = await testSubjects.findAll('dataStreams-filter-option'); - return options.length === 3; // Wait until exactly 3 options are available - }); - - // Retrieve all the filter options const options = await testSubjects.findAll('dataStreams-filter-option'); // Assert that exactly 3 elements are present @@ -96,5 +117,64 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Assert the badge displays the expected count expect(activeFiltersCount).to.be('3'); }); + it('renders charts', async () => { + // data is coming from the mocked autoops API + const chartContainer = await testSubjects.find('data-usage-metrics'); + await testSubjects.existOrFail('data-usage-metrics'); + + // check 2 charts rendered + await retry.waitFor('chart to render', async () => { + const chartStatus = await chartContainer.findAllByCssSelector( + '.echChartStatus[data-ech-render-complete="true"]' + ); + return chartStatus.length === 2; + }); + }); + it('renders legend and actions popover', async () => { + const ingestRateChart = await testSubjects.find('ingest_rate-chart'); + const storageRetainedChart = await testSubjects.find('storage_retained-chart'); + + // Verify legend items for the ingest_rate chart + const ingestLegendItems = await ingestRateChart.findAllByCssSelector('li.echLegendItem'); + expect(ingestLegendItems.length).to.eql(4); // 3 data streams + 1 Total line series + + const ingestLegendNames = await Promise.all( + ingestLegendItems.map(async (item) => item.getAttribute('data-ech-series-name')) + ); + + expect(ingestLegendNames.sort()).to.eql( + [ + 'metrics-system.cpu-default', + 'metrics-system.core.total.pct-default', + 'logs-nginx.access-default', + 'Total', + ].sort() + ); + + const storageLegendItems = await storageRetainedChart.findAllByCssSelector( + 'li.echLegendItem' + ); + expect(storageLegendItems.length).to.eql(4); // same number of data streams + total line series + + const storageLegendNames = await Promise.all( + storageLegendItems.map(async (item) => item.getAttribute('data-ech-series-name')) + ); + + expect(storageLegendNames.sort()).to.eql( + [ + 'metrics-system.cpu-default', + 'metrics-system.core.total.pct-default', + 'logs-nginx.access-default', + 'Total', + ].sort() + ); + // actions menu + const firstLegendItem = ingestLegendItems[0]; + const actionButton = await firstLegendItem.findByTestSubject('legendActionButton'); + await actionButton.click(); + + // Verify that the popover now appears + await testSubjects.existOrFail('legendActionPopover'); + }); }); }; From 39520dca07146bc0fc86916be95aab635d430ff5 Mon Sep 17 00:00:00 2001 From: neptunian Date: Mon, 9 Dec 2024 11:30:22 -0500 Subject: [PATCH 3/5] add tests --- .../app/components/dataset_quality_link.tsx | 6 +- .../public/app/components/legend_action.tsx | 2 + .../app/components/legend_action_item.tsx | 12 +- .../functional/page_objects/index.ts | 2 + .../functional/page_objects/svl_data_usage.ts | 55 ++++++++ .../test_suites/common/data_usage/main.ts | 123 +++++++++++------- 6 files changed, 147 insertions(+), 53 deletions(-) create mode 100644 x-pack/test_serverless/functional/page_objects/svl_data_usage.ts diff --git a/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx b/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx index 8e81e6091156b..3b481565ae27c 100644 --- a/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx +++ b/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx @@ -41,7 +41,11 @@ export const DatasetQualityLink: React.FC = React.memo( } }; return ( - + ); } ); diff --git a/x-pack/plugins/data_usage/public/app/components/legend_action.tsx b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx index 6d2df9d78f070..1282bd43e863a 100644 --- a/x-pack/plugins/data_usage/public/app/components/legend_action.tsx +++ b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx @@ -73,11 +73,13 @@ export const LegendAction: React.FC = React.memo( {hasIndexManagementFeature && ( )} {hasDataSetQualityFeature && } diff --git a/x-pack/plugins/data_usage/public/app/components/legend_action_item.tsx b/x-pack/plugins/data_usage/public/app/components/legend_action_item.tsx index 3b4f0d9f698f7..542fff6902fb9 100644 --- a/x-pack/plugins/data_usage/public/app/components/legend_action_item.tsx +++ b/x-pack/plugins/data_usage/public/app/components/legend_action_item.tsx @@ -9,9 +9,15 @@ import React, { memo } from 'react'; import { EuiListGroupItem } from '@elastic/eui'; export const LegendActionItem = memo( - ({ label, onClick }: { label: string; onClick: () => Promise | void }) => ( - - ) + ({ + label, + onClick, + dataTestSubj, + }: { + label: string; + onClick: () => Promise | void; + dataTestSubj: string; + }) => ); LegendActionItem.displayName = 'LegendActionItem'; diff --git a/x-pack/test_serverless/functional/page_objects/index.ts b/x-pack/test_serverless/functional/page_objects/index.ts index e10e98529b8bf..5c00c95b73a1d 100644 --- a/x-pack/test_serverless/functional/page_objects/index.ts +++ b/x-pack/test_serverless/functional/page_objects/index.ts @@ -26,6 +26,7 @@ import { SvlSearchElasticsearchStartPageProvider } from './svl_search_elasticsea import { SvlApiKeysProvider } from './svl_api_keys'; import { SvlSearchCreateIndexPageProvider } from './svl_search_create_index_page'; import { SvlSearchInferenceManagementPageProvider } from './svl_search_inference_management_page'; +import { SvlDataUsagePageProvider } from './svl_data_usage'; export const pageObjects = { ...xpackFunctionalPageObjects, @@ -49,4 +50,5 @@ export const pageObjects = { svlApiKeys: SvlApiKeysProvider, svlSearchCreateIndexPage: SvlSearchCreateIndexPageProvider, svlSearchInferenceManagementPage: SvlSearchInferenceManagementPageProvider, + svlDataUsagePage: SvlDataUsagePageProvider, }; diff --git a/x-pack/test_serverless/functional/page_objects/svl_data_usage.ts b/x-pack/test_serverless/functional/page_objects/svl_data_usage.ts new file mode 100644 index 0000000000000..0f684c24fcae0 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/svl_data_usage.ts @@ -0,0 +1,55 @@ +/* + * 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 { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SvlDataUsagePageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertDataUsagePageExists(): Promise { + return await testSubjects.exists('DataUsagePage'); + }, + async clickDatastreamsDropdown() { + await testSubjects.click('data-usage-metrics-filter-dataStreams-popoverButton'); + }, + async findDatastreamsDropdownOptions() { + return await testSubjects.findAll('dataStreams-filter-option'); + }, + async findDatastreamsDropdownFilterButton() { + return await testSubjects.find('data-usage-metrics-filter-dataStreams-popoverButton'); + }, + async findIngestRateChart() { + return await testSubjects.find('ingest_rate-chart'); + }, + async storageRetainedChart() { + return await testSubjects.find('storage_retained-chart'); + }, + async findLegendItemsInChart(chartElement: WebElementWrapper) { + return await chartElement.findAllByCssSelector('li.echLegendItem'); + }, + async findLegendActionButton(legendItemElement: WebElementWrapper) { + return legendItemElement.findByTestSubject('legendActionButton'); + }, + async clickLegendActionButtonAtIndex(chartElement: WebElementWrapper, index: number) { + const legendItems = await this.findLegendItemsInChart(chartElement); + if (index < 0 || index >= legendItems.length) { + throw new Error( + `Invalid legend item index: ${index}. There are only ${legendItems.length} legend items.` + ); + } + const legendItem = legendItems[index]; + const actionButton = await this.findLegendActionButton(legendItem); + await actionButton.click(); + }, + + async assertLegendActionPopoverExists() { + await testSubjects.existOrFail('legendActionPopover'); + }, + }; +} diff --git a/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts b/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts index e70e8ce3a8ce8..72d738e15f36c 100644 --- a/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts +++ b/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts @@ -11,12 +11,18 @@ import { interceptRequest } from './intercept_request'; import { setupMockServer } from '../../../../api_integration/test_suites/common/data_usage/mock_api'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - const pageObjects = getPageObjects(['svlCommonPage', 'svlManagementPage', 'common']); + const pageObjects = getPageObjects([ + 'svlDataUsagePage', + 'svlCommonPage', + 'svlManagementPage', + 'common', + ]); const testSubjects = getService('testSubjects'); const retry = getService('retry'); const driver = getService('__webdriver__'); const mockAutoopsApiService = setupMockServer(); const es = getService('es'); + const browser = getService('browser'); let mockApiServer: http.Server; const dataStreamsMockResponse = [ @@ -85,14 +91,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders data usage page', async () => { await retry.waitFor('page to be visible', async () => { - return await testSubjects.exists('DataUsagePage'); + return await pageObjects.svlDataUsagePage.assertDataUsagePageExists(); }); }); it('shows 3 data streams in the filter dropdown', async () => { // Click the dropdown button to show the options - await testSubjects.click('data-usage-metrics-filter-dataStreams-popoverButton'); + await pageObjects.svlDataUsagePage.clickDatastreamsDropdown(); - const options = await testSubjects.findAll('dataStreams-filter-option'); + const options = await pageObjects.svlDataUsagePage.findDatastreamsDropdownOptions(); // Assert that exactly 3 elements are present expect(options.length).to.eql(3); @@ -104,12 +110,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } // Locate the filter button using its data-test-subj - const filterButton = await testSubjects.find( - 'data-usage-metrics-filter-dataStreams-popoverButton' - ); + const datastreamsDropdownFilterButton = + await pageObjects.svlDataUsagePage.findDatastreamsDropdownFilterButton(); // Find the badge element within the button (using its CSS class) - const notificationBadge = await filterButton.findByCssSelector('.euiNotificationBadge'); + const notificationBadge = await datastreamsDropdownFilterButton.findByCssSelector( + '.euiNotificationBadge' + ); // Retrieve the text content of the badge const activeFiltersCount = await notificationBadge.getVisibleText(); @@ -118,11 +125,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(activeFiltersCount).to.be('3'); }); it('renders charts', async () => { - // data is coming from the mocked autoops API + // Data is coming from the mocked autoops API const chartContainer = await testSubjects.find('data-usage-metrics'); await testSubjects.existOrFail('data-usage-metrics'); - // check 2 charts rendered + // Check 2 charts rendered await retry.waitFor('chart to render', async () => { const chartStatus = await chartContainer.findAllByCssSelector( '.echChartStatus[data-ech-render-complete="true"]' @@ -130,51 +137,69 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return chartStatus.length === 2; }); }); - it('renders legend and actions popover', async () => { - const ingestRateChart = await testSubjects.find('ingest_rate-chart'); - const storageRetainedChart = await testSubjects.find('storage_retained-chart'); + it('renders legend', async () => { + const ingestRateChart = await pageObjects.svlDataUsagePage.findIngestRateChart(); + const storageRetainedChart = await pageObjects.svlDataUsagePage.storageRetainedChart(); - // Verify legend items for the ingest_rate chart - const ingestLegendItems = await ingestRateChart.findAllByCssSelector('li.echLegendItem'); - expect(ingestLegendItems.length).to.eql(4); // 3 data streams + 1 Total line series - - const ingestLegendNames = await Promise.all( - ingestLegendItems.map(async (item) => item.getAttribute('data-ech-series-name')) + const ingestLegendItems = await pageObjects.svlDataUsagePage.findLegendItemsInChart( + ingestRateChart ); - expect(ingestLegendNames.sort()).to.eql( - [ - 'metrics-system.cpu-default', - 'metrics-system.core.total.pct-default', - 'logs-nginx.access-default', - 'Total', - ].sort() - ); + expect(ingestLegendItems.length).to.eql(4); // 3 data streams + 1 Total line series - const storageLegendItems = await storageRetainedChart.findAllByCssSelector( - 'li.echLegendItem' + const storageLegendItems = await pageObjects.svlDataUsagePage.findLegendItemsInChart( + storageRetainedChart ); expect(storageLegendItems.length).to.eql(4); // same number of data streams + total line series - - const storageLegendNames = await Promise.all( - storageLegendItems.map(async (item) => item.getAttribute('data-ech-series-name')) - ); - - expect(storageLegendNames.sort()).to.eql( - [ - 'metrics-system.cpu-default', - 'metrics-system.core.total.pct-default', - 'logs-nginx.access-default', - 'Total', - ].sort() - ); - // actions menu - const firstLegendItem = ingestLegendItems[0]; - const actionButton = await firstLegendItem.findByTestSubject('legendActionButton'); - await actionButton.click(); - - // Verify that the popover now appears - await testSubjects.existOrFail('legendActionPopover'); + }); + it('renders actions popover with correct links', async () => { + // Open the first legend item actions popover + const ingestRateChart = await pageObjects.svlDataUsagePage.findIngestRateChart(); + await pageObjects.svlDataUsagePage.clickLegendActionButtonAtIndex(ingestRateChart, 0); + await pageObjects.svlDataUsagePage.assertLegendActionPopoverExists(); + // Check for links + await testSubjects.existOrFail('copyDataStreamNameAction'); + await testSubjects.existOrFail('manageDataStreamAction'); + await testSubjects.existOrFail('DatasetQualityAction'); + + const manageLink = await testSubjects.find('manageDataStreamAction'); + await manageLink.click(); + + // Wait for navigation to the data stream details page + await retry.waitFor('URL to update (index management)', async () => { + const currentUrl = await browser.getCurrentUrl(); + return currentUrl.includes( + `/app/management/data/index_management/data_streams/${dataStreamsMockResponse[0].name}` + ); + }); + await browser.goBack(); + // test second link to ensure state changed + await pageObjects.svlDataUsagePage.clickLegendActionButtonAtIndex(ingestRateChart, 1); + await pageObjects.svlDataUsagePage.assertLegendActionPopoverExists(); + + await manageLink.click(); + + // Wait for navigation to the data stream details page + await retry.waitFor('URL to update (index management)', async () => { + const currentUrl = await browser.getCurrentUrl(); + return currentUrl.includes( + `/app/management/data/index_management/data_streams/${dataStreamsMockResponse[1].name}` + ); + }); + await browser.goBack(); + + // Test navigation for the data quality link + await pageObjects.svlDataUsagePage.clickLegendActionButtonAtIndex(ingestRateChart, 0); + await pageObjects.svlDataUsagePage.assertLegendActionPopoverExists(); + const dataQualityLink = await testSubjects.find('DatasetQualityAction'); + await dataQualityLink.click(); + + // Wait for navigation to the data quality details page + await retry.waitFor('URL to update (data quality)', async () => { + const currentUrl = await browser.getCurrentUrl(); + return currentUrl.includes('/app/management/data/data_quality/details'); + }); + await browser.goBack(); }); }); }; From 411aafbd15c0eafbea574f38ef9efc3466105e8a Mon Sep 17 00:00:00 2001 From: neptunian Date: Mon, 9 Dec 2024 13:10:36 -0500 Subject: [PATCH 4/5] codeowners --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 032c8f17a98c6..9331163b6417c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2121,6 +2121,7 @@ x-pack/test_serverless/functional/test_suites/security/index.mki_only.ts @elasti /x-pack/test/functional/es_archives/auditbeat/default @elastic/security-solution /x-pack/test/functional/es_archives/auditbeat/hosts @elastic/security-solution /x-pack/test_serverless/functional/page_objects/svl_management_page.ts @elastic/security-solution +/x-pack/test_serverless/functional/page_objects/svl_data_usage.ts @elastic/security-solution @elastic/obs-ai-assistant /x-pack/test_serverless/api_integration/test_suites/security @elastic/security-solution /x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts @elastic/security-solution From 20268c6991b633ec41839057cdc30ab0222f9202 Mon Sep 17 00:00:00 2001 From: neptunian Date: Tue, 10 Dec 2024 16:13:43 -0500 Subject: [PATCH 5/5] add interceptRequest to browser service --- .../index.ts | 2 +- .../services/browser.ts | 61 ++++++++++++++++- .../common/data_usage/intercept_request.ts | 68 ------------------- .../test_suites/common/data_usage/main.ts | 8 +-- 4 files changed, 64 insertions(+), 75 deletions(-) delete mode 100644 x-pack/test_serverless/functional/test_suites/common/data_usage/intercept_request.ts diff --git a/packages/kbn-ftr-common-functional-ui-services/index.ts b/packages/kbn-ftr-common-functional-ui-services/index.ts index 1355d112ffaf8..c316ecf7db60e 100644 --- a/packages/kbn-ftr-common-functional-ui-services/index.ts +++ b/packages/kbn-ftr-common-functional-ui-services/index.ts @@ -15,7 +15,7 @@ export type { CustomCheerioStatic, } from './services/web_element_wrapper/custom_cheerio_api'; export { Browsers } from './services/remote/browsers'; -export { type Browser } from './services/browser'; +export { type Browser, type InterceptResponseFactory } from './services/browser'; export { NETWORK_PROFILES, type NetworkOptions, diff --git a/packages/kbn-ftr-common-functional-ui-services/services/browser.ts b/packages/kbn-ftr-common-functional-ui-services/services/browser.ts index 56888b0a8087c..7a3885d7e3f9a 100644 --- a/packages/kbn-ftr-common-functional-ui-services/services/browser.ts +++ b/packages/kbn-ftr-common-functional-ui-services/services/browser.ts @@ -13,6 +13,7 @@ import { Key, Origin, type WebDriver } from 'selenium-webdriver'; import { Driver as ChromiumWebDriver } from 'selenium-webdriver/chrome'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; import Url from 'url'; +import { Protocol } from 'devtools-protocol'; import { NoSuchSessionError } from 'selenium-webdriver/lib/error'; import sharp from 'sharp'; @@ -26,7 +27,12 @@ import { import { FtrService, type FtrProviderContext } from './ftr_provider_context'; export type Browser = BrowserService; - +export interface InterceptResponseFactory { + fail: () => ['Fetch.failRequest', Protocol.Fetch.FailRequestRequest]; + fulfill: ( + responseOptions: Omit + ) => ['Fetch.fulfillRequest', Protocol.Fetch.FulfillRequestRequest]; +} class BrowserService extends FtrService { /** * Keyboard events @@ -837,6 +843,59 @@ class BrowserService extends FtrService { throw new Error(message); } } + + /** + * Intercept network requests using the Chrome DevTools Protocol (CDP). + * @param pattern - URL pattern to match intercepted requests. + * @param onIntercept - Callback defining how to handle intercepted requests. + * @param cb - Callback to trigger actions that make requests. + */ + + public async interceptRequest( + pattern: string, + onIntercept: (responseFactory: InterceptResponseFactory) => [string, Record], + cb: () => Promise + ): Promise { + const connection = await this.driver.createCDPConnection('page'); + + return new Promise((resolve, reject) => { + connection._wsConnection.on('message', async (data: Buffer) => { + try { + const parsed = JSON.parse(data.toString()); + this.log.debug(`CDP Event: ${parsed.method} ${parsed.params?.request?.url}`); + + if (parsed.method === 'Fetch.requestPaused') { + const requestId = parsed.params.requestId; + + const [method, params] = onIntercept({ + fail: () => ['Fetch.failRequest', { requestId, errorReason: 'Failed' }], + fulfill: (responseOptions) => [ + 'Fetch.fulfillRequest', + { requestId, ...responseOptions }, + ], + }); + + connection.execute(method, params, () => { + this.log.debug(`Executed command: ${method}`); + }); + } + } catch (error) { + this.log.error(`Error in Fetch.requestPaused handler: ${error.message}`); + } + }); + + connection.execute('Fetch.enable', { patterns: [{ urlPattern: pattern }] }, (result: any) => { + this.log.debug('Fetch.enable result:', result); + + cb() + .then(resolve) + .catch((error) => { + this.log.error(`Error in callback: ${error.message}`); + reject(error); + }); + }); + }); + } } export async function BrowserProvider(ctx: FtrProviderContext) { diff --git a/x-pack/test_serverless/functional/test_suites/common/data_usage/intercept_request.ts b/x-pack/test_serverless/functional/test_suites/common/data_usage/intercept_request.ts deleted file mode 100644 index eb703def4864e..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/common/data_usage/intercept_request.ts +++ /dev/null @@ -1,68 +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 { WebDriver } from 'selenium-webdriver'; - -export interface ResponseFactory { - fail: (reason?: string) => [string, Record]; - fulfill: (responseOptions: Record) => [string, Record]; -} - -export async function interceptRequest( - driver: WebDriver, - pattern: string, - onIntercept: (responseFactory: ResponseFactory) => [string, Record], - cb: () => Promise -) { - const connection = await driver.createCDPConnection('page'); - - return new Promise((resolve, reject) => { - connection._wsConnection.on('message', async (data: Buffer) => { - const parsed = JSON.parse(data.toString()); - - if (parsed.method === 'Fetch.requestPaused') { - await new Promise((innerResolve) => - connection.execute( - ...onIntercept({ - fail: () => [ - 'Fetch.failRequest', - { requestId: parsed.params.requestId, errorReason: 'Failed' }, - ], - fulfill: (responseOptions: any) => [ - 'Fetch.fulfillRequest', - { - requestId: parsed.params.requestId, - ...responseOptions, - }, - ], - }), - innerResolve - ) - ); - - await new Promise((innerResolve) => connection.execute('Fetch.disable', {}, innerResolve)); - resolve(); - } - }); - - new Promise((innerResolve) => - connection.execute( - 'Fetch.enable', - { - patterns: [{ urlPattern: pattern }], - }, - innerResolve - ) - ) - .then(() => { - return cb(); - }) - .catch((err: Error) => { - reject(err); - }); - }); -} diff --git a/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts b/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts index 72d738e15f36c..438bf5ab7ee84 100644 --- a/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts +++ b/x-pack/test_serverless/functional/test_suites/common/data_usage/main.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; import http from 'http'; +import { InterceptResponseFactory } from '@kbn/ftr-common-functional-ui-services'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { interceptRequest } from './intercept_request'; import { setupMockServer } from '../../../../api_integration/test_suites/common/data_usage/mock_api'; export default ({ getPageObjects, getService }: FtrProviderContext) => { @@ -19,7 +19,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); const testSubjects = getService('testSubjects'); const retry = getService('retry'); - const driver = getService('__webdriver__'); const mockAutoopsApiService = setupMockServer(); const es = getService('es'); const browser = getService('browser'); @@ -67,10 +66,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // intercept the data_streams request to bypass waiting for the metering api to aggregate a response // otherwise storage sizes get filtered out if they are 0 - await interceptRequest( - driver.driver, + await browser.interceptRequest( '*data_streams*', - (responseFactory) => { + (responseFactory: InterceptResponseFactory) => { return responseFactory.fulfill({ responseCode: 200, responseHeaders: [{ name: 'Content-Type', value: 'application/json' }],