chore(streams): add scout test for wired and classic stream (#220295)

This commit is contained in:
Kevin Delemme 2025-05-23 08:37:51 -04:00 committed by GitHub
parent a4940f5a78
commit 9650da839f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 501 additions and 4 deletions

View file

@ -7,4 +7,5 @@ ui_tests:
- observability_onboarding
- painless_lab
- security_solution
- streams_app
disabled:

View file

@ -9,9 +9,11 @@
import { coreWorkerFixtures } from '../core_fixtures';
import { FleetApiService, getFleetApiHelper } from './fleet';
import { StreamsApiService, getStreamsApiService } from './streams';
export interface ApiServicesFixture {
fleet: FleetApiService;
streams: StreamsApiService;
// add more services here
}
@ -26,6 +28,7 @@ export const apiServicesFixture = coreWorkerFixtures.extend<
async ({ kbnClient, log }, use) => {
const services = {
fleet: getFleetApiHelper(log, kbnClient),
streams: getStreamsApiService({ kbnClient, log }),
};
log.serviceLoaded('apiServices');

View file

@ -0,0 +1,47 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { KbnClient, ScoutLogger, measurePerformanceAsync } from '../../../../../common';
import { ScoutParallelWorkerFixtures } from '../../../parallel_run_fixtures';
export interface StreamsApiService {
enable: () => Promise<void>;
disable: () => Promise<void>;
}
export const getStreamsApiService = ({
kbnClient,
log,
scoutSpace,
}: {
kbnClient: KbnClient;
log: ScoutLogger;
scoutSpace?: ScoutParallelWorkerFixtures['scoutSpace'];
}): StreamsApiService => {
const basePath = scoutSpace?.id ? `/s/${scoutSpace?.id}` : '';
return {
enable: async () => {
await measurePerformanceAsync(log, 'streamsApi.enable', async () => {
await kbnClient.request({
method: 'POST',
path: `${basePath}/api/streams/_enable`,
});
});
},
disable: async () => {
await measurePerformanceAsync(log, 'streamsApi.disable', async () => {
await kbnClient.request({
method: 'POST',
path: `${basePath}/api/streams/_disable`,
});
});
},
};
};

View file

@ -170,6 +170,7 @@ export function RetentionMetadata({
</EuiFlexGroup>
}
button={contextualMenu}
dataTestSubj="streamsAppRetentionMetadataRetentionPeriod"
/>
<EuiHorizontalRule margin="s" />
<MetadataRow
@ -238,14 +239,21 @@ function MetadataRow({
value,
tip,
button,
dataTestSubj,
}: {
metadata: string;
value: ReactNode;
tip?: string;
button?: ReactNode;
dataTestSubj?: string;
}) {
return (
<EuiFlexGroup alignItems="center" gutterSize="xl" responsive={false}>
<EuiFlexGroup
alignItems="center"
gutterSize="xl"
responsive={false}
data-test-subj={dataTestSubj}
>
<EuiFlexItem grow={1}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>

View file

@ -207,6 +207,7 @@ export function AddDashboardFlyout({
loading={dashboardSuggestionsFetch.loading}
selectedDashboards={selectedDashboards}
setSelectedDashboards={setSelectedDashboards}
dataTestSubj="streamsAppAddDashboardFlyoutDashboardsTable"
/>
</EuiFlexGroup>
</EuiFlyoutBody>

View file

@ -27,6 +27,7 @@ export function DashboardsTable({
setSelectedDashboards,
loading,
entityId,
dataTestSubj,
}: {
entityId?: string;
loading: boolean;
@ -34,6 +35,7 @@ export function DashboardsTable({
compact?: boolean;
selectedDashboards?: SanitizedDashboardAsset[];
setSelectedDashboards?: (dashboards: SanitizedDashboardAsset[]) => void;
dataTestSubj?: string;
}) {
const {
core: { application },
@ -58,7 +60,7 @@ export function DashboardsTable({
}),
render: (_, { title, id }) => (
<EuiLink
data-test-subj="streamsAppColumnsLink"
data-test-subj="streamsAppDashboardColumnsLink"
onClick={() => {
if (entityId) {
telemetryClient.trackAssetClick({
@ -117,6 +119,7 @@ export function DashboardsTable({
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false} />
<EuiBasicTable
data-test-subj={dataTestSubj}
columns={columns}
itemId="id"
items={items}

View file

@ -210,6 +210,7 @@ export function StreamDetailDashboardsView({
loading={dashboardsFetch.loading}
selectedDashboards={selectedDashboards}
setSelectedDashboards={canLinkAssets ? setSelectedDashboards : undefined}
dataTestSubj="streamsAppStreamDetailDashboardsTable"
/>
{definition && isAddDashboardFlyoutOpen ? (
<AddDashboardFlyout

View file

@ -26,6 +26,7 @@ export function StreamsEmptyPrompt() {
return (
<EuiEmptyPrompt
data-test-subj="streamsEmptyPrompt"
icon={<AssetImage type="noResults" />}
title={
<h2>

View file

@ -9,7 +9,8 @@
"typings/**/*",
"public/**/*.json",
"server/**/*",
".storybook/**/*"
".storybook/**/*",
"ui_tests/**/*"
],
"exclude": ["target/**/*", ".storybook/**/*.js"],
"kbn_references": [
@ -68,6 +69,7 @@
"@kbn/ingest-pipelines-plugin",
"@kbn/deeplinks-observability",
"@kbn/content-packs-schema",
"@kbn/charts-theme"
"@kbn/charts-theme",
"@kbn/scout"
]
}

View file

@ -0,0 +1,25 @@
## How to run tests
First start the servers:
```bash
// ESS
node scripts/scout.js start-server --stateful
// Serverless
node scripts/scout.js start-server --serverless=[es|oblt|security]
```
Then you can run the tests in another terminal:
Some tests are designed to run sequentially:
```bash
// ESS
npx playwright test --config x-pack/platform/plugins/shared/streams_app/ui_tests/playwright.config.ts --project=local --grep @ess
// Serverless
npx playwright test --config x-pack/platform/plugins/shared/streams_app/ui_tests/playwright.config.ts --project=local --grep @svlOblt
```
Test results are available in `x-pack/platform/plugins/shared/streams_app/ui_tests/output`

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export const KBN_ARCHIVES = {
DASHBOARD: 'x-pack/test/functional/fixtures/kbn_archiver/dashboard/simple.json',
};

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ScoutPage, ScoutTestFixtures, ScoutWorkerFixtures, test as baseTest } from '@kbn/scout';
import { StreamsPageObjects, extendPageObjects } from './page_objects';
export interface StreamsTestFixtures extends ScoutTestFixtures {
pageObjects: StreamsPageObjects;
}
export const test = baseTest.extend<StreamsTestFixtures, ScoutWorkerFixtures>({
pageObjects: async (
{
pageObjects,
page,
}: {
pageObjects: StreamsPageObjects;
page: ScoutPage;
},
use: (pageObjects: StreamsPageObjects) => Promise<void>
) => {
const extendedPageObjects = extendPageObjects(pageObjects, page);
await use(extendedPageObjects);
},
});
export * as testData from './constants';

View file

@ -0,0 +1,20 @@
/*
* 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 { PageObjects, ScoutPage, createLazyPageObject } from '@kbn/scout';
import { StreamsApp } from './streams_app';
export interface StreamsPageObjects extends PageObjects {
streams: StreamsApp;
}
export function extendPageObjects(pageObjects: PageObjects, page: ScoutPage): StreamsPageObjects {
return {
...pageObjects,
streams: createLazyPageObject(StreamsApp, page),
};
}

View file

@ -0,0 +1,61 @@
/*
* 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 { ScoutPage, expect } from '@kbn/scout';
export class StreamsApp {
constructor(private readonly page: ScoutPage) {}
async goto() {
await this.page.gotoApp('streams');
await expect(this.page.getByText('StreamsTechnical Preview')).toBeVisible();
}
async gotoStreamsFromBreadcrumb() {
await this.page
.getByTestId('breadcrumbs')
.getByRole('link', { name: 'Streams', exact: true })
.click();
}
async gotoStream(stream: string) {
const last = await this.page.getByTestId('breadcrumb last').textContent();
if (last !== 'Streams') {
await this.gotoStreamsFromBreadcrumb();
}
await this.page.getByRole('link', { name: stream, exact: true }).click();
}
async gotoStreamDashboard(stream: string) {
await this.gotoStream(stream);
await this.page.getByRole('tab', { name: 'Dashboards' }).click();
}
async gotoManageStream(stream: string) {
this.gotoStream(stream);
await this.page.getByRole('link', { name: 'Manage stream' }).click();
}
async gotoCreateChildStream(parent: string) {
await this.gotoManageStream(parent);
await this.page.getByRole('button', { name: 'Create child stream' }).click();
}
async gotoDataRetentionTab(stream: string) {
await this.gotoManageStream(stream);
await this.page.getByRole('tab', { name: 'Data retention' }).click();
}
async gotoExtractFieldTab(stream: string) {
await this.gotoManageStream(stream);
await this.page.getByRole('tab', { name: 'Extract field' }).click();
}
async gotoSchemaEditorTab(stream: string) {
await this.gotoManageStream(stream);
await this.page.getByRole('tab', { name: 'Schema editor' }).click();
}
}

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createPlaywrightConfig } from '@kbn/scout';
// eslint-disable-next-line import/no-default-export
export default createPlaywrightConfig({
testDir: './tests',
});

View file

@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { expect } from '@kbn/scout';
import { testData, test } from '../fixtures';
const DATA_STREAM_NAME = 'my-data-stream';
test.describe('Classic Streams', { tag: ['@ess', '@svlOblt'] }, () => {
test.beforeEach(async ({ kbnClient, esClient, browserAuth, pageObjects }) => {
await kbnClient.importExport.load(testData.KBN_ARCHIVES.DASHBOARD);
await esClient.indices.putIndexTemplate({
name: 'my-index-template',
index_patterns: [`${DATA_STREAM_NAME}*`],
data_stream: {},
priority: 500,
template: {
lifecycle: {
data_retention: '7d',
},
},
});
await esClient.indices.createDataStream({
name: DATA_STREAM_NAME,
});
await esClient.index({
index: DATA_STREAM_NAME,
document: {
'@timestamp': '2025-05-01T00:00:00.000Z',
message: 'GET /search HTTP/1.1 200 1070000',
'agent.name': 'nginx',
},
refresh: 'wait_for',
});
await browserAuth.loginAsAdmin();
await pageObjects.streams.goto();
});
test.afterAll(async ({ kbnClient, esClient, apiServices }) => {
await esClient.indices.deleteDataStream({ name: DATA_STREAM_NAME });
await esClient.indices.deleteIndexTemplate({
name: 'my-index-template',
});
await kbnClient.savedObjects.cleanStandardList();
});
test('full flow', async ({ page, esClient, pageObjects }) => {
// Update data retention
await pageObjects.streams.gotoDataRetentionTab(DATA_STREAM_NAME);
await page.getByRole('button', { name: 'Edit data retention' }).click();
await page.getByRole('button', { name: 'Set specific retention days' }).click();
await page.getByTestId('streamsAppDslModalDaysField').fill('30');
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByTestId('streamsAppRetentionMetadataRetentionPeriod').getByText('30d')
).toBeVisible();
await page.getByTestId('toastCloseButton').click();
// Update field extraction
await pageObjects.streams.gotoExtractFieldTab(DATA_STREAM_NAME);
await page.getByText('Add a processor').click();
await page.locator('input[name="field"]').fill('message');
await page
.locator('input[name="patterns\\.0\\.value"]')
.fill('%{WORD:method} %{URIPATH:request}');
await page.getByRole('button', { name: 'Add processor' }).click();
await page.getByRole('button', { name: 'Save changes' }).click();
await expect(page.getByText("Stream's processors updated")).toBeVisible();
await page.getByTestId('toastCloseButton').click();
// Add dashboard
await pageObjects.streams.gotoStreamDashboard(DATA_STREAM_NAME);
await page.getByRole('button', { name: 'Add a dashboard' }).click();
await expect(
page
.getByTestId('streamsAppAddDashboardFlyoutDashboardsTable')
.getByRole('button', { name: 'Some Dashboard' })
).toBeVisible();
// eslint-disable-next-line playwright/no-nth-methods
await page.getByRole('cell', { name: 'Select row' }).locator('div').first().click();
await page.getByRole('button', { name: 'Add dashboard' }).click();
await expect(
page
.getByTestId('streamsAppStreamDetailDashboardsTable')
.getByTestId('streamsAppDashboardColumnsLink')
).toHaveText('Some Dashboard');
// remove dashboard
await page
.getByTestId('streamsAppStreamDetailDashboardsTable')
.getByRole('cell', { name: 'Select row' })
.locator('div')
// eslint-disable-next-line playwright/no-nth-methods
.first()
.click();
await page.getByRole('button', { name: 'Unlink selected' }).click();
await expect(
page.getByTestId('streamsAppStreamDetailDashboardsTable').getByText('No items found')
).toBeVisible();
});
});

View file

@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { expect } from '@kbn/scout';
import { testData, test } from '../fixtures';
test.describe('Wired Streams', { tag: ['@ess', '@svlOblt'] }, () => {
test.beforeEach(async ({ apiServices, kbnClient, browserAuth, pageObjects }) => {
await kbnClient.importExport.load(testData.KBN_ARCHIVES.DASHBOARD);
await apiServices.streams.enable();
await browserAuth.loginAsAdmin();
await pageObjects.streams.goto();
});
test.afterAll(async ({ kbnClient, apiServices }) => {
await apiServices.streams.disable();
await kbnClient.savedObjects.cleanStandardList();
});
test('full flow', async ({ page, esClient, pageObjects }) => {
await pageObjects.streams.gotoCreateChildStream('logs');
await page.getByLabel('Stream name').fill('logs.nginx');
await page.getByPlaceholder('Field').fill('agent.name');
await page.getByPlaceholder('Value').fill('nginx');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('link', { name: 'logs.nginx', exact: true })).toBeVisible();
await page.getByTestId('toastCloseButton').click();
// Update "logs.nginx" data retention
await pageObjects.streams.gotoDataRetentionTab('logs.nginx');
await page.getByRole('button', { name: 'Edit data retention' }).click();
await page.getByRole('button', { name: 'Set specific retention days' }).click();
await page.getByTestId('streamsAppDslModalDaysField').fill('7');
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByTestId('streamsAppRetentionMetadataRetentionPeriod').getByText('7d')
).toBeVisible();
await page.getByTestId('toastCloseButton').click();
// Update "logs.nginx" processing
await esClient.index({
index: 'logs',
document: {
'@timestamp': '2025-05-01T00:00:00.000Z',
message: JSON.stringify({
'log.level': 'info',
'log.logger': 'nginx',
message: 'GET /search HTTP/1.1 200 1070000',
}),
'agent.name': 'nginx',
'other.field': 'important',
},
refresh: 'wait_for',
});
await pageObjects.streams.gotoExtractFieldTab('logs.nginx');
await page.getByText('Add a processor').click();
await page.locator('input[name="field"]').fill('message');
await page
.locator('input[name="patterns\\.0\\.value"]')
.fill('%{WORD:method} %{URIPATH:request}');
await page.getByRole('button', { name: 'Add processor' }).click();
await page.getByRole('button', { name: 'Save changes' }).click();
await expect(page.getByText("Stream's processors updated")).toBeVisible();
await page.getByTestId('toastCloseButton').click();
// Update "logs.nginx" mapping
await pageObjects.streams.gotoSchemaEditorTab('logs.nginx');
await page
.getByRole('row', { name: 'agent.name ----- ----- logs.' })
.getByLabel('Open actions menu')
.click();
await page
.getByRole('row', { name: 'agent.name ----- ----- logs.' })
.getByLabel('Open actions menu')
.click();
await page.getByRole('button', { name: 'Map field' }).click();
await page.getByRole('combobox').selectOption('keyword');
await page.getByRole('button', { name: 'Save changes' }).click();
await page.getByRole('heading', { name: 'agent.name' }).waitFor({ state: 'hidden' });
await expect(page.getByText('Mapped', { exact: true })).toBeVisible();
await page.getByTestId('toastCloseButton').click();
// Add dashboard
await pageObjects.streams.gotoStreamDashboard('logs.nginx');
await page.getByRole('button', { name: 'Add a dashboard' }).click();
await expect(
page
.getByTestId('streamsAppAddDashboardFlyoutDashboardsTable')
.getByRole('button', { name: 'Some Dashboard' })
).toBeVisible();
// eslint-disable-next-line playwright/no-nth-methods
await page.getByRole('cell', { name: 'Select row' }).locator('div').first().click();
await page.getByRole('button', { name: 'Add dashboard' }).click();
await expect(
page
.getByTestId('streamsAppStreamDetailDashboardsTable')
.getByTestId('streamsAppDashboardColumnsLink')
).toHaveText('Some Dashboard');
// remove dashboard
await page
.getByTestId('streamsAppStreamDetailDashboardsTable')
.getByRole('cell', { name: 'Select row' })
.locator('div')
// eslint-disable-next-line playwright/no-nth-methods
.first()
.click();
await page.getByRole('button', { name: 'Unlink selected' }).click();
await expect(
page.getByTestId('streamsAppStreamDetailDashboardsTable').getByText('No items found')
).toBeVisible();
});
});

View file

@ -0,0 +1,36 @@
{
"attributes": {
"controlGroupInput": {
"chainingSystem": "HIERARCHICAL",
"controlStyle": "oneLine",
"ignoreParentSettingsJSON": "{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}",
"panelsJSON": "{}",
"showApplySelections": false
},
"description": "Some dashboard",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"}}"
},
"optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}",
"panelsJSON": "[]",
"timeRestore": false,
"title": "Some Dashboard",
"version": 3
},
"coreMigrationVersion": "8.8.0",
"created_at": "2025-05-09T14:33:00.927Z",
"created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0",
"id": "a1395240-bc20-4cc7-8559-349d049622d2",
"managed": false,
"references": [
{
"id": "64b8958c-e914-48ff-a31d-fc51227093b8",
"name": "tag-ref-64b8958c-e914-48ff-a31d-fc51227093b8",
"type": "tag"
}
],
"type": "dashboard",
"typeMigrationVersion": "10.2.0",
"updated_at": "2025-05-09T14:33:00.927Z",
"version": "WzE2LDFd"
}