diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index c4d455fb43b7f..fc246641101e3 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -127,9 +127,9 @@ export function AlertsFlyout({ ]; return ( - + - +

{alertData.fields[ALERT_RULE_NAME]}

@@ -141,13 +141,27 @@ export function AlertsFlyout({ compressed={true} type="responsiveColumn" listItems={overviewListItems} + titleProps={ + { + 'data-test-subj': 'alertsFlyoutDescriptionListTitle', + } as any // NOTE / TODO: This "any" is a temporary workaround: https://github.com/elastic/eui/issues/5148 + } + descriptionProps={ + { + 'data-test-subj': 'alertsFlyoutDescriptionListDescription', + } as any // NOTE / TODO: This "any" is a temporary workaround: https://github.com/elastic/eui/issues/5148 + } /> {alertData.link && !isInApp && ( - + View in app diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index 2d325b6f3f7c4..bca8c8095511e 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -254,6 +254,7 @@ function ObservabilityActions({ iconType="expand" color="text" onClick={() => setFlyoutAlert(alert)} + data-test-subj="openFlyoutButton" /> diff --git a/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx index 324a97ff2a39f..f4a9158a3e4e7 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx @@ -51,7 +51,7 @@ export const TGridEmpty: React.FC<{ height?: keyof typeof heights }> = ({ height const { http } = useKibana().services; return ( - + diff --git a/x-pack/test/functional/es_archives/observability/alerts/data.json.gz b/x-pack/test/functional/es_archives/observability/alerts/data.json.gz index 45da368188284..bcccaeda999c0 100644 Binary files a/x-pack/test/functional/es_archives/observability/alerts/data.json.gz and b/x-pack/test/functional/es_archives/observability/alerts/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/observability/alerts/mappings.json b/x-pack/test/functional/es_archives/observability/alerts/mappings.json index 88d12b7d797bb..63750ddafe329 100644 --- a/x-pack/test/functional/es_archives/observability/alerts/mappings.json +++ b/x-pack/test/functional/es_archives/observability/alerts/mappings.json @@ -65,8 +65,12 @@ } } }, - "id": { - "type": "keyword" + "instance": { + "properties": { + "id": { + "type": "keyword" + } + } }, "reason": { "type": "keyword" @@ -325,8 +329,12 @@ } } }, - "id": { - "type": "keyword" + "instance": { + "properties": { + "id": { + "type": "keyword" + } + } }, "reason": { "type": "keyword" @@ -561,8 +569,12 @@ } } }, - "id": { - "type": "keyword" + "instance": { + "properties": { + "id": { + "type": "keyword" + } + } }, "reason": { "type": "keyword" diff --git a/x-pack/test/functional/services/observability/alerts.ts b/x-pack/test/functional/services/observability/alerts.ts new file mode 100644 index 0000000000000..ba7f952b30c64 --- /dev/null +++ b/x-pack/test/functional/services/observability/alerts.ts @@ -0,0 +1,129 @@ +/* + * 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 querystring from 'querystring'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; + +// Based on the x-pack/test/functional/es_archives/observability/alerts archive. +const DATE_WITH_DATA = { + rangeFrom: '2021-09-01T13:36:22.109Z', + rangeTo: '2021-09-03T13:36:22.109Z', +}; + +const ALERTS_FLYOUT_SELECTOR = 'alertsFlyout'; + +export function ObservabilityAlertsProvider({ getPageObjects, getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const flyoutService = getService('flyout'); + const pageObjects = getPageObjects(['common']); + const retry = getService('retry'); + + const navigateToTimeWithData = async () => { + return await pageObjects.common.navigateToUrlWithBrowserHistory( + 'observability', + '/alerts', + `?${querystring.stringify(DATE_WITH_DATA)}` + ); + }; + + const getTableCells = async () => { + // NOTE: This isn't ideal, but EuiDataGrid doesn't really have the concept of "rows" + return await testSubjects.findAll('dataGridRowCell'); + }; + + const getTableOrFail = async () => { + return await testSubjects.existOrFail('events-viewer-panel'); + }; + + const getNoDataStateOrFail = async () => { + return await testSubjects.existOrFail('tGridEmptyState'); + }; + + // Query Bar + const getQueryBar = async () => { + return await testSubjects.find('queryInput'); + }; + + const getQuerySubmitButton = async () => { + return await testSubjects.find('querySubmitButton'); + }; + + const clearQueryBar = async () => { + return await (await getQueryBar()).clearValueWithKeyboard({ charByChar: true }); + }; + + const typeInQueryBar = async (query: string) => { + return await (await getQueryBar()).type(query); + }; + + const submitQuery = async (query: string) => { + await typeInQueryBar(query); + return await (await getQuerySubmitButton()).click(); + }; + + // Flyout + const getOpenFlyoutButton = async () => { + return await testSubjects.find('openFlyoutButton'); + }; + + const openAlertsFlyout = async () => { + await (await getOpenFlyoutButton()).click(); + await retry.waitFor( + 'flyout open', + async () => await testSubjects.exists(ALERTS_FLYOUT_SELECTOR, { timeout: 2500 }) + ); + }; + + const getAlertsFlyout = async () => { + return await testSubjects.find(ALERTS_FLYOUT_SELECTOR); + }; + + const getAlertsFlyoutOrFail = async () => { + return await testSubjects.existOrFail(ALERTS_FLYOUT_SELECTOR); + }; + + const getAlertsFlyoutTitle = async () => { + return await testSubjects.find('alertsFlyoutTitle'); + }; + + const closeAlertsFlyout = async () => { + return await flyoutService.close(ALERTS_FLYOUT_SELECTOR); + }; + + const getAlertsFlyoutViewInAppButtonOrFail = async () => { + return await testSubjects.existOrFail('alertsFlyoutViewInAppButton'); + }; + + const getAlertsFlyoutDescriptionListTitles = async (): Promise => { + const flyout = await getAlertsFlyout(); + return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListTitle', flyout); + }; + + const getAlertsFlyoutDescriptionListDescriptions = async (): Promise => { + const flyout = await getAlertsFlyout(); + return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListDescription', flyout); + }; + + return { + clearQueryBar, + typeInQueryBar, + submitQuery, + getTableCells, + getTableOrFail, + getNoDataStateOrFail, + openAlertsFlyout, + getAlertsFlyout, + getAlertsFlyoutTitle, + closeAlertsFlyout, + navigateToTimeWithData, + getAlertsFlyoutOrFail, + getAlertsFlyoutViewInAppButtonOrFail, + getAlertsFlyoutDescriptionListTitles, + getAlertsFlyoutDescriptionListDescriptions, + }; +} diff --git a/x-pack/test/functional/services/observability/index.ts b/x-pack/test/functional/services/observability/index.ts index 14f931d93b56f..0d167ae5d516e 100644 --- a/x-pack/test/functional/services/observability/index.ts +++ b/x-pack/test/functional/services/observability/index.ts @@ -7,11 +7,14 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { ObservabilityUsersProvider } from './users'; +import { ObservabilityAlertsProvider } from './alerts'; export function ObservabilityProvider(context: FtrProviderContext) { const users = ObservabilityUsersProvider(context); + const alerts = ObservabilityAlertsProvider(context); return { users, + alerts, }; } diff --git a/x-pack/test/observability_functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/alerts/index.ts index ae60eff1859ba..d4ed8fdd674f7 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -6,32 +6,28 @@ */ import expect from '@kbn/expect'; -import querystring from 'querystring'; import { FtrProviderContext } from '../../../ftr_provider_context'; -// Based on the x-pack/test/functional/es_archives/observability/alerts archive. -const DATE_WITH_DATA = { - rangeFrom: '2021-08-31T13:36:22.109Z', - rangeTo: '2021-09-01T13:36:22.109Z', -}; +async function asyncForEach(array: T[], callback: (item: T, index: number) => void) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index); + } +} export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); - // FLAKY: https://github.com/elastic/kibana/issues/110920 - describe.skip('Observability alerts', function () { + describe('Observability alerts', function () { this.tags('includeFirefox'); const pageObjects = getPageObjects(['common']); const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const observability = getService('observability'); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); - await pageObjects.common.navigateToUrlWithBrowserHistory( - 'observability', - '/alerts', - `?${querystring.stringify(DATE_WITH_DATA)}` - ); + await observability.alerts.navigateToTimeWithData(); }); after(async () => { @@ -40,13 +36,123 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Alerts table', () => { it('Renders the table', async () => { - await testSubjects.existOrFail('events-viewer-panel'); + await observability.alerts.getTableOrFail(); }); it('Renders the correct number of cells', async () => { // NOTE: This isn't ideal, but EuiDataGrid doesn't really have the concept of "rows" - const cells = await testSubjects.findAll('dataGridRowCell'); - expect(cells.length).to.be(54); + const cells = await observability.alerts.getTableCells(); + expect(cells.length).to.be(72); + }); + + describe('Filtering', () => { + afterEach(async () => { + await observability.alerts.clearQueryBar(); + }); + + after(async () => { + // NOTE: We do this as the query bar takes the place of the datepicker when it is in focus, so we'll reset + // back to default. + await observability.alerts.submitQuery(''); + }); + + it('Autocompletion works', async () => { + await observability.alerts.typeInQueryBar('kibana.alert.s'); + await testSubjects.existOrFail('autocompleteSuggestion-field-kibana.alert.start-'); + await testSubjects.existOrFail('autocompleteSuggestion-field-kibana.alert.status-'); + }); + + it('Applies filters correctly', async () => { + await observability.alerts.submitQuery('kibana.alert.status: recovered'); + await retry.try(async () => { + const cells = await observability.alerts.getTableCells(); + expect(cells.length).to.be(24); + }); + }); + + it('Displays a no data state when filters produce zero results', async () => { + await observability.alerts.submitQuery('kibana.alert.consumer: uptime'); + await observability.alerts.getNoDataStateOrFail(); + }); + }); + + describe('Date selection', () => { + after(async () => { + await observability.alerts.navigateToTimeWithData(); + }); + + it('Correctly applies date picker selections', async () => { + await retry.try(async () => { + await (await testSubjects.find('superDatePickerToggleQuickMenuButton')).click(); + // We shouldn't expect any data for the last 15 minutes + await (await testSubjects.find('superDatePickerCommonlyUsed_Last_15 minutes')).click(); + }); + await observability.alerts.getNoDataStateOrFail(); + await pageObjects.common.waitUntilUrlIncludes('rangeFrom=now-15m&rangeTo=now'); + }); + }); + + describe('Flyout', () => { + it('Can be opened', async () => { + await observability.alerts.openAlertsFlyout(); + await observability.alerts.getAlertsFlyoutOrFail(); + }); + + it('Can be closed', async () => { + await observability.alerts.closeAlertsFlyout(); + await testSubjects.missingOrFail('alertsFlyout'); + }); + + describe('When open', async () => { + before(async () => { + await observability.alerts.openAlertsFlyout(); + }); + + after(async () => { + await observability.alerts.closeAlertsFlyout(); + }); + + it('Displays the correct title', async () => { + const titleText = await ( + await observability.alerts.getAlertsFlyoutTitle() + ).getVisibleText(); + expect(titleText).to.contain('Log threshold'); + }); + + it('Displays the correct content', async () => { + const flyoutTitles = await observability.alerts.getAlertsFlyoutDescriptionListTitles(); + const flyoutDescriptions = await observability.alerts.getAlertsFlyoutDescriptionListDescriptions(); + + const expectedTitles = [ + 'Status', + 'Last updated', + 'Duration', + 'Expected value', + 'Actual value', + 'Rule type', + ]; + const expectedDescriptions = [ + 'Active', + 'Sep 2, 2021 @ 12:54:09.674', + '15 minutes', + '100.25', + '1957', + 'Log threshold', + ]; + + await asyncForEach(flyoutTitles, async (title, index) => { + expect(await title.getVisibleText()).to.be(expectedTitles[index]); + }); + + await asyncForEach(flyoutDescriptions, async (description, index) => { + expect(await description.getVisibleText()).to.be(expectedDescriptions[index]); + }); + }); + + it('Displays a View in App button', async () => { + await observability.alerts.getAlertsFlyoutViewInAppButtonOrFail(); + }); + }); }); }); });