Skip to content

Commit

Permalink
[Data Usage] functional tests (elastic#203166)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
neptunian authored Dec 12, 2024
1 parent c75ae63 commit ba945c9
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 12 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -2128,6 +2128,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
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-ftr-common-functional-ui-services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<Protocol.Fetch.FulfillRequestRequest, 'requestId'>
) => ['Fetch.fulfillRequest', Protocol.Fetch.FulfillRequestRequest];
}
class BrowserService extends FtrService {
/**
* Keyboard events
Expand Down Expand Up @@ -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<string, any>],
cb: () => Promise<void>
): Promise<void> {
const connection = await this.driver.createCDPConnection('page');

return new Promise<void>((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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const ChartPanel: React.FC<ChartPanelProps> = ({

return (
<EuiFlexItem grow={false} key={metricType}>
<EuiPanel hasShadow={false} hasBorder={true}>
<EuiPanel hasShadow={false} hasBorder={true} data-test-subj={`${metricType}-chart`}>
<EuiTitle size="xs">
<h5>{chartKeyToTitleMap[metricType as ChartKey] || metricType}</h5>
</EuiTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ export const DatasetQualityLink: React.FC<DatasetQualityLinkProps> = React.memo(
}
};
return (
<LegendActionItem label={UX_LABELS.dataQualityPopup.view} onClick={onClickDataQuality} />
<LegendActionItem
label={UX_LABELS.dataQualityPopup.view}
onClick={onClickDataQuality}
dataTestSubj="DatasetQualityAction"
/>
);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ export const LegendAction: React.FC<LegendActionProps> = React.memo(
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiPopover
data-test-subj="legendActionPopover"
button={
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="boxesHorizontal"
aria-label={UX_LABELS.dataQualityPopup.open}
onClick={() => togglePopover(uniqueStreamName)}
data-test-subj="legendActionButton"
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand All @@ -71,11 +73,13 @@ export const LegendAction: React.FC<LegendActionProps> = React.memo(
<LegendActionItem
label={UX_LABELS.dataQualityPopup.copy}
onClick={onCopyDataStreamName}
dataTestSubj="copyDataStreamNameAction"
/>
{hasIndexManagementFeature && (
<LegendActionItem
label={UX_LABELS.dataQualityPopup.manage}
onClick={onClickIndexManagement}
dataTestSubj="manageDataStreamAction"
/>
)}
{hasDataSetQualityFeature && <DatasetQualityLink dataStreamName={label} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> | void }) => (
<EuiListGroupItem label={label} onClick={onClick} size="s" />
)
({
label,
onClick,
dataTestSubj,
}: {
label: string;
onClick: () => Promise<void> | void;
dataTestSubj: string;
}) => <EuiListGroupItem label={label} onClick={onClick} data-test-subj={dataTestSubj} size="s" />
);

LegendActionItem.displayName = 'LegendActionItem';
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
2 changes: 2 additions & 0 deletions x-pack/test_serverless/functional/page_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -49,4 +50,5 @@ export const pageObjects = {
svlApiKeys: SvlApiKeysProvider,
svlSearchCreateIndexPage: SvlSearchCreateIndexPageProvider,
svlSearchInferenceManagementPage: SvlSearchInferenceManagementPageProvider,
svlDataUsagePage: SvlDataUsagePageProvider,
};
55 changes: 55 additions & 0 deletions x-pack/test_serverless/functional/page_objects/svl_data_usage.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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');
},
};
}
Loading

0 comments on commit ba945c9

Please sign in to comment.