Skip to content

Commit ba945c9

Browse files
authored
[Data Usage] functional tests (elastic#203166)
## Summary Functional tests for data usage UI. - `data_streams` route is intercepted, due to filtering out zero size data streams which will happen because metering api needs time to aggregate data - `autoops_api` is using the mock server as there will be no data for it to return - tests will only run in local serverless and not MKI due to using the autoops mock server that won't return data for created data streams - adds `interceptRequest` functionality to FTR `browser` service ## Tests - data stream filter dropdown renders with created data streams of `data_streams` response and are checked - data stream filter dropdown renders badge with correct number of selected data streams - charts render from `data_streams` route response - chart legends render with correct items - popover renders for legend items - links in popovers correctly navigate and update navigation between different data stream items
1 parent c75ae63 commit ba945c9

File tree

12 files changed

+329
-12
lines changed

12 files changed

+329
-12
lines changed

.github/CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -2128,6 +2128,7 @@ x-pack/test_serverless/functional/test_suites/security/index.mki_only.ts @elasti
21282128
/x-pack/test/functional/es_archives/auditbeat/default @elastic/security-solution
21292129
/x-pack/test/functional/es_archives/auditbeat/hosts @elastic/security-solution
21302130
/x-pack/test_serverless/functional/page_objects/svl_management_page.ts @elastic/security-solution
2131+
/x-pack/test_serverless/functional/page_objects/svl_data_usage.ts @elastic/security-solution @elastic/obs-ai-assistant
21312132
/x-pack/test_serverless/api_integration/test_suites/security @elastic/security-solution
21322133

21332134
/x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts @elastic/security-solution

packages/kbn-ftr-common-functional-ui-services/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type {
1515
CustomCheerioStatic,
1616
} from './services/web_element_wrapper/custom_cheerio_api';
1717
export { Browsers } from './services/remote/browsers';
18-
export { type Browser } from './services/browser';
18+
export { type Browser, type InterceptResponseFactory } from './services/browser';
1919
export {
2020
NETWORK_PROFILES,
2121
type NetworkOptions,

packages/kbn-ftr-common-functional-ui-services/services/browser.ts

+60-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Key, Origin, type WebDriver } from 'selenium-webdriver';
1313
import { Driver as ChromiumWebDriver } from 'selenium-webdriver/chrome';
1414
import { setTimeout as setTimeoutAsync } from 'timers/promises';
1515
import Url from 'url';
16+
import { Protocol } from 'devtools-protocol';
1617

1718
import { NoSuchSessionError } from 'selenium-webdriver/lib/error';
1819
import sharp from 'sharp';
@@ -26,7 +27,12 @@ import {
2627
import { FtrService, type FtrProviderContext } from './ftr_provider_context';
2728

2829
export type Browser = BrowserService;
29-
30+
export interface InterceptResponseFactory {
31+
fail: () => ['Fetch.failRequest', Protocol.Fetch.FailRequestRequest];
32+
fulfill: (
33+
responseOptions: Omit<Protocol.Fetch.FulfillRequestRequest, 'requestId'>
34+
) => ['Fetch.fulfillRequest', Protocol.Fetch.FulfillRequestRequest];
35+
}
3036
class BrowserService extends FtrService {
3137
/**
3238
* Keyboard events
@@ -837,6 +843,59 @@ class BrowserService extends FtrService {
837843
throw new Error(message);
838844
}
839845
}
846+
847+
/**
848+
* Intercept network requests using the Chrome DevTools Protocol (CDP).
849+
* @param pattern - URL pattern to match intercepted requests.
850+
* @param onIntercept - Callback defining how to handle intercepted requests.
851+
* @param cb - Callback to trigger actions that make requests.
852+
*/
853+
854+
public async interceptRequest(
855+
pattern: string,
856+
onIntercept: (responseFactory: InterceptResponseFactory) => [string, Record<string, any>],
857+
cb: () => Promise<void>
858+
): Promise<void> {
859+
const connection = await this.driver.createCDPConnection('page');
860+
861+
return new Promise<void>((resolve, reject) => {
862+
connection._wsConnection.on('message', async (data: Buffer) => {
863+
try {
864+
const parsed = JSON.parse(data.toString());
865+
this.log.debug(`CDP Event: ${parsed.method} ${parsed.params?.request?.url}`);
866+
867+
if (parsed.method === 'Fetch.requestPaused') {
868+
const requestId = parsed.params.requestId;
869+
870+
const [method, params] = onIntercept({
871+
fail: () => ['Fetch.failRequest', { requestId, errorReason: 'Failed' }],
872+
fulfill: (responseOptions) => [
873+
'Fetch.fulfillRequest',
874+
{ requestId, ...responseOptions },
875+
],
876+
});
877+
878+
connection.execute(method, params, () => {
879+
this.log.debug(`Executed command: ${method}`);
880+
});
881+
}
882+
} catch (error) {
883+
this.log.error(`Error in Fetch.requestPaused handler: ${error.message}`);
884+
}
885+
});
886+
887+
connection.execute('Fetch.enable', { patterns: [{ urlPattern: pattern }] }, (result: any) => {
888+
this.log.debug('Fetch.enable result:', result);
889+
890+
cb()
891+
.then(resolve)
892+
.catch((error) => {
893+
this.log.error(`Error in callback: ${error.message}`);
894+
reject(error);
895+
});
896+
});
897+
});
898+
}
840899
}
841900

842901
export async function BrowserProvider(ctx: FtrProviderContext) {

x-pack/platform/plugins/private/data_usage/public/app/components/chart_panel.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({
9191

9292
return (
9393
<EuiFlexItem grow={false} key={metricType}>
94-
<EuiPanel hasShadow={false} hasBorder={true}>
94+
<EuiPanel hasShadow={false} hasBorder={true} data-test-subj={`${metricType}-chart`}>
9595
<EuiTitle size="xs">
9696
<h5>{chartKeyToTitleMap[metricType as ChartKey] || metricType}</h5>
9797
</EuiTitle>

x-pack/platform/plugins/private/data_usage/public/app/components/dataset_quality_link.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ export const DatasetQualityLink: React.FC<DatasetQualityLinkProps> = React.memo(
4141
}
4242
};
4343
return (
44-
<LegendActionItem label={UX_LABELS.dataQualityPopup.view} onClick={onClickDataQuality} />
44+
<LegendActionItem
45+
label={UX_LABELS.dataQualityPopup.view}
46+
onClick={onClickDataQuality}
47+
dataTestSubj="DatasetQualityAction"
48+
/>
4549
);
4650
}
4751
);

x-pack/platform/plugins/private/data_usage/public/app/components/legend_action.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,15 @@ export const LegendAction: React.FC<LegendActionProps> = React.memo(
5252
return (
5353
<EuiFlexGroup gutterSize="s" alignItems="center">
5454
<EuiPopover
55+
data-test-subj="legendActionPopover"
5556
button={
5657
<EuiFlexGroup gutterSize="s" alignItems="center">
5758
<EuiFlexItem grow={false}>
5859
<EuiButtonIcon
5960
iconType="boxesHorizontal"
6061
aria-label={UX_LABELS.dataQualityPopup.open}
6162
onClick={() => togglePopover(uniqueStreamName)}
63+
data-test-subj="legendActionButton"
6264
/>
6365
</EuiFlexItem>
6466
</EuiFlexGroup>
@@ -71,11 +73,13 @@ export const LegendAction: React.FC<LegendActionProps> = React.memo(
7173
<LegendActionItem
7274
label={UX_LABELS.dataQualityPopup.copy}
7375
onClick={onCopyDataStreamName}
76+
dataTestSubj="copyDataStreamNameAction"
7477
/>
7578
{hasIndexManagementFeature && (
7679
<LegendActionItem
7780
label={UX_LABELS.dataQualityPopup.manage}
7881
onClick={onClickIndexManagement}
82+
dataTestSubj="manageDataStreamAction"
7983
/>
8084
)}
8185
{hasDataSetQualityFeature && <DatasetQualityLink dataStreamName={label} />}

x-pack/platform/plugins/private/data_usage/public/app/components/legend_action_item.tsx

+9-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ import React, { memo } from 'react';
99
import { EuiListGroupItem } from '@elastic/eui';
1010

1111
export const LegendActionItem = memo(
12-
({ label, onClick }: { label: string; onClick: () => Promise<void> | void }) => (
13-
<EuiListGroupItem label={label} onClick={onClick} size="s" />
14-
)
12+
({
13+
label,
14+
onClick,
15+
dataTestSubj,
16+
}: {
17+
label: string;
18+
onClick: () => Promise<void> | void;
19+
dataTestSubj: string;
20+
}) => <EuiListGroupItem label={label} onClick={onClick} data-test-subj={dataTestSubj} size="s" />
1521
);
1622

1723
LegendActionItem.displayName = 'LegendActionItem';

x-pack/test_serverless/api_integration/test_suites/common/data_usage/mock_data.ts

+16
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export const mockAutoOpsResponse = {
1515
[1726862130000, 14657904],
1616
],
1717
},
18+
{
19+
name: 'metrics-system.core.total.pct-default',
20+
error: null,
21+
data: [
22+
[1726858530000, 13756849],
23+
[1726862130000, 14657904],
24+
],
25+
},
1826
{
1927
name: 'logs-nginx.access-default',
2028
error: null,
@@ -33,6 +41,14 @@ export const mockAutoOpsResponse = {
3341
[1726862130000, 13956423],
3442
],
3543
},
44+
{
45+
name: 'metrics-system.core.total.pct-default',
46+
error: null,
47+
data: [
48+
[1726858530000, 13756849],
49+
[1726862130000, 14657904],
50+
],
51+
},
3652
{
3753
name: 'logs-nginx.access-default',
3854
error: null,

x-pack/test_serverless/api_integration/test_suites/common/data_usage/tests/metrics.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) {
2929
const mockAutoopsApiService = setupMockServer();
3030
describe('Metrics', function () {
3131
let mockApiServer: http.Server;
32-
// due to the plugin depending on yml config (xpack.dataUsage.enabled), we cannot test in MKI until it is on by default
32+
// MKI has a different config in the QA environment and will ignore the mock service
3333
this.tags(['skipMKI']);
3434

3535
before(async () => {

x-pack/test_serverless/functional/page_objects/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { SvlSearchElasticsearchStartPageProvider } from './svl_search_elasticsea
2626
import { SvlApiKeysProvider } from './svl_api_keys';
2727
import { SvlSearchCreateIndexPageProvider } from './svl_search_create_index_page';
2828
import { SvlSearchInferenceManagementPageProvider } from './svl_search_inference_management_page';
29+
import { SvlDataUsagePageProvider } from './svl_data_usage';
2930

3031
export const pageObjects = {
3132
...xpackFunctionalPageObjects,
@@ -49,4 +50,5 @@ export const pageObjects = {
4950
svlApiKeys: SvlApiKeysProvider,
5051
svlSearchCreateIndexPage: SvlSearchCreateIndexPageProvider,
5152
svlSearchInferenceManagementPage: SvlSearchInferenceManagementPageProvider,
53+
svlDataUsagePage: SvlDataUsagePageProvider,
5254
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
9+
import { FtrProviderContext } from '../ftr_provider_context';
10+
11+
export function SvlDataUsagePageProvider({ getService }: FtrProviderContext) {
12+
const testSubjects = getService('testSubjects');
13+
14+
return {
15+
async assertDataUsagePageExists(): Promise<boolean> {
16+
return await testSubjects.exists('DataUsagePage');
17+
},
18+
async clickDatastreamsDropdown() {
19+
await testSubjects.click('data-usage-metrics-filter-dataStreams-popoverButton');
20+
},
21+
async findDatastreamsDropdownOptions() {
22+
return await testSubjects.findAll('dataStreams-filter-option');
23+
},
24+
async findDatastreamsDropdownFilterButton() {
25+
return await testSubjects.find('data-usage-metrics-filter-dataStreams-popoverButton');
26+
},
27+
async findIngestRateChart() {
28+
return await testSubjects.find('ingest_rate-chart');
29+
},
30+
async storageRetainedChart() {
31+
return await testSubjects.find('storage_retained-chart');
32+
},
33+
async findLegendItemsInChart(chartElement: WebElementWrapper) {
34+
return await chartElement.findAllByCssSelector('li.echLegendItem');
35+
},
36+
async findLegendActionButton(legendItemElement: WebElementWrapper) {
37+
return legendItemElement.findByTestSubject('legendActionButton');
38+
},
39+
async clickLegendActionButtonAtIndex(chartElement: WebElementWrapper, index: number) {
40+
const legendItems = await this.findLegendItemsInChart(chartElement);
41+
if (index < 0 || index >= legendItems.length) {
42+
throw new Error(
43+
`Invalid legend item index: ${index}. There are only ${legendItems.length} legend items.`
44+
);
45+
}
46+
const legendItem = legendItems[index];
47+
const actionButton = await this.findLegendActionButton(legendItem);
48+
await actionButton.click();
49+
},
50+
51+
async assertLegendActionPopoverExists() {
52+
await testSubjects.existOrFail('legendActionPopover');
53+
},
54+
};
55+
}

0 commit comments

Comments
 (0)