[Detection Engine] Cypress - Add more robust selection from our DataView dropdown component (#213510)

This addresses some recent cypress failures: 

* https://github.com/elastic/kibana/issues/212743 (Rule Creation with a
DataView)
* https://github.com/elastic/kibana/issues/212742 (Rule Creation + Edit
with a DataView)
* https://github.com/elastic/kibana/issues/213752 (Rule Creation +
Filter with a DataView)

This appears (as much as a cypress failure can 😓) to be caused by an
incorrect/false-positive assertion, leading to us (very occasionally)
interacting with the combobox before it's ready. We were calling
`.should('not.be.disabled')` on an element that could never be disabled.
By calling that instead on the inner `input` that actually is
enabled/disabled, we have the sanity check that was originally intended.

This PR also adds a post-action check (`.should('contains',
thingThatWasTyped)`) so that if the action fails, the test doesn't fail
inscrutably at a later step.

### Evidence

*
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/8003
(50x)
*
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/8004
(200x)
*
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/8033
(200x)

### Significance

**Note also** that some initial investigation found this pattern in
several places in our test suite. I'm going to follow up on this focused
PR with a more comprehensive one (once this is proven out in the flaky
runner).

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
This commit is contained in:
Ryland Herrick 2025-03-14 11:10:27 -05:00 committed by GitHub
parent 3df90c8f2a
commit 02409dbd65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 201 additions and 127 deletions

View file

@ -79,129 +79,124 @@ import { fillAddFilterForm } from '../../../../tasks/search_bar';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
// Skipping in MKI due to flake
// Failing: See https://github.com/elastic/kibana/issues/212743
describe.skip(
'Custom query rules',
{ tags: ['@ess', '@serverless', '@skipInServerlessMKI'] },
() => {
describe('Custom detection rules creation with data views', () => {
const rule = getDataViewRule();
const expectedUrls = rule.references?.join('');
const expectedFalsePositives = rule.false_positives?.join('');
const expectedTags = rule.tags?.join('');
const mitreAttack = rule.threat;
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
const expectedNumberOfRules = 1;
describe('Custom query rules', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => {
describe('Custom detection rules creation with data views', () => {
const rule = getDataViewRule();
const expectedUrls = rule.references?.join('');
const expectedFalsePositives = rule.false_positives?.join('');
const expectedTags = rule.tags?.join('');
const mitreAttack = rule.threat;
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
const expectedNumberOfRules = 1;
beforeEach(() => {
if (rule.data_view_id != null) {
postDataView(rule.data_view_id);
}
deleteAlertsAndRules();
login();
});
afterEach(() => {
if (rule.data_view_id != null) {
deleteDataView(rule.data_view_id);
}
});
it('Creates and enables a new rule', function () {
visit(CREATE_RULE_URL);
fillDefineCustomRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
getRulesManagementTableRows().should('have.length', expectedNumberOfRules);
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
cy.get(SEVERITY).should('have.text', 'High');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'High');
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(DATA_VIEW_DETAILS).should('have.text', rule.data_view_id);
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
cy.get(DEFINITION_DETAILS).should('not.contain', INDEX_PATTERNS_DETAILS);
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${rule.interval}`);
const humanizedDuration = getHumanizedDuration(
rule.from ?? 'now-6m',
rule.interval ?? '5m'
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT)
.invoke('text')
.should('match', /^[1-9].+$/);
cy.get(ALERT_GRID_CELL).contains(rule.name);
});
it('Creates and edits a new rule with a data view', function () {
visit(CREATE_RULE_URL);
fillDefineCustomRuleAndContinue(rule);
cy.get(RULE_NAME_INPUT).clear();
cy.get(RULE_NAME_INPUT).type(rule.name);
cy.get(RULE_DESCRIPTION_INPUT).clear();
cy.get(RULE_DESCRIPTION_INPUT).type(rule.description);
cy.get(ABOUT_CONTINUE_BTN).should('exist').click();
fillScheduleRuleAndContinue(rule);
createRuleWithoutEnabling();
openRuleManagementPageViaBreadcrumbs();
goToRuleDetailsOf(rule.name);
cy.get(EDIT_RULE_SETTINGS_LINK).click();
cy.get(RULE_NAME_HEADER).should('contain', 'Edit rule settings');
});
it('Adds filter on define step', () => {
visit(CREATE_RULE_URL);
fillDefineCustomRule(rule);
openAddFilterPopover();
fillAddFilterForm({
key: 'host.name',
operator: 'exists',
});
// Check that newly added filter exists
cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should('have.text', 'host.name: exists');
});
beforeEach(() => {
if (rule.data_view_id != null) {
postDataView(rule.data_view_id);
}
deleteAlertsAndRules();
login();
});
}
);
afterEach(() => {
if (rule.data_view_id != null) {
deleteDataView(rule.data_view_id);
}
});
it('Creates and enables a new rule', function () {
visit(CREATE_RULE_URL);
fillDefineCustomRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
getRulesManagementTableRows().should('have.length', expectedNumberOfRules);
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
cy.get(SEVERITY).should('have.text', 'High');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'High');
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(DATA_VIEW_DETAILS).should('have.text', rule.data_view_id);
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
cy.get(DEFINITION_DETAILS).should('not.contain', INDEX_PATTERNS_DETAILS);
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${rule.interval}`);
const humanizedDuration = getHumanizedDuration(
rule.from ?? 'now-6m',
rule.interval ?? '5m'
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT)
.invoke('text')
.should('match', /^[1-9].+$/);
cy.get(ALERT_GRID_CELL).contains(rule.name);
});
it('Creates and edits a new rule with a data view', function () {
visit(CREATE_RULE_URL);
fillDefineCustomRuleAndContinue(rule);
cy.get(RULE_NAME_INPUT).clear();
cy.get(RULE_NAME_INPUT).type(rule.name);
cy.get(RULE_DESCRIPTION_INPUT).clear();
cy.get(RULE_DESCRIPTION_INPUT).type(rule.description);
cy.get(ABOUT_CONTINUE_BTN).should('exist').click();
fillScheduleRuleAndContinue(rule);
createRuleWithoutEnabling();
openRuleManagementPageViaBreadcrumbs();
goToRuleDetailsOf(rule.name);
cy.get(EDIT_RULE_SETTINGS_LINK).click();
cy.get(RULE_NAME_HEADER).should('contain', 'Edit rule settings');
});
it('Adds filter on define step', () => {
visit(CREATE_RULE_URL);
fillDefineCustomRule(rule);
openAddFilterPopover();
fillAddFilterForm({
key: 'host.name',
operator: 'exists',
});
// Check that newly added filter exists
cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should('have.text', 'host.name: exists');
});
});
});

View file

@ -108,8 +108,7 @@ export const CUSTOM_QUERY_REQUIRED = 'A custom query is required.';
export const THREAT_MATCH_QUERY_REQUIRED = 'An indicator index query is required.';
export const DATA_VIEW_COMBO_BOX =
'[data-test-subj="pick-rule-data-source"] [data-test-subj="comboBoxInput"]';
export const DATA_VIEW_COMBO_BOX = '[data-test-subj="pick-rule-data-source"]';
export const DATA_VIEW_OPTION = '[data-test-subj="rule-index-toggle-dataView"]';

View file

@ -0,0 +1,33 @@
/*
* 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 EUI_COMBO_BOX_SELECTOR = '[data-test-subj="comboBoxInput"]';
export const EUI_COMBO_BOX_SELECTIONS_SELECTOR = `${EUI_COMBO_BOX_SELECTOR} [data-test-subj="euiComboBoxPill"]`;
export const EUI_COMBO_BOX_INPUT_SELECTOR = `${EUI_COMBO_BOX_SELECTOR} input`;
/**
* @param parentSelector CSS Selector targeting the parent EuiComboBox component
* @returns A selector targeting the inner combobox element to be interacted with
*/
export const getComboBoxSelector = (parentSelector?: string): string =>
`${parentSelector} ${EUI_COMBO_BOX_SELECTOR}`;
/**
* @param parentSelector CSS Selector targeting the parent EuiComboBox component
* @returns A selector targeting the actual combobox <input /> element
*/
export const getComboBoxInputSelector = (parentSelector?: string): string =>
`${parentSelector} ${EUI_COMBO_BOX_INPUT_SELECTOR}`;
/**
* @param parentSelector CSS Selector targeting the parent EuiComboBox component
* @returns A selector targeting the selected options on the EuiComboBox
*/
export const getComboBoxSelectionsSelector = (parentSelector?: string): string =>
`${parentSelector} ${EUI_COMBO_BOX_SELECTIONS_SELECTOR}`;

View file

@ -159,6 +159,7 @@ import { waitForAlerts } from './alerts';
import { refreshPage } from './security_header';
import { COMBO_BOX_OPTION, TOOLTIP } from '../screens/common';
import { EMPTY_ALERT_TABLE } from '../screens/alerts';
import { fillComboBox } from './eui_form_interactions';
export const createAndEnableRule = () => {
cy.get(CREATE_AND_ENABLE_BTN).click();
@ -463,7 +464,7 @@ export const removeAlertsIndex = () => {
export const fillDefineCustomRule = (rule: QueryRuleCreateProps) => {
if (rule.data_view_id !== undefined) {
cy.get(DATA_VIEW_OPTION).click();
cy.get(DATA_VIEW_COMBO_BOX).type(`${rule.data_view_id}{enter}`);
fillComboBox({ parentSelector: DATA_VIEW_COMBO_BOX, options: rule.data_view_id });
}
cy.get(CUSTOM_QUERY_INPUT)
.first()

View file

@ -0,0 +1,46 @@
/*
* 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 {
getComboBoxInputSelector,
getComboBoxSelectionsSelector,
getComboBoxSelector,
} from '../screens/eui_form_interactions';
/**
* Fills an EuiComboBox in a robust way. Ensures that the component is ready to
* be interacted with, and also that each option was successfully chosen.
*
* @param parentSelector CSS Selector targeting the parent EuiComboBox component
* @param options A string (or strings) to be chosen from the EuiComboBox
*/
export const fillComboBox = ({
parentSelector,
options,
}: {
parentSelector?: string;
options: string | string[];
}) => {
const _options = options instanceof Array ? options : [options];
const comboBoxSelector = getComboBoxSelector(parentSelector);
const comboBoxInputSelector = getComboBoxInputSelector(parentSelector);
const comboBoxSelectionsSelector = getComboBoxSelectionsSelector(parentSelector);
cy.get(comboBoxInputSelector).should('not.be.disabled');
_options.forEach((option, index) => {
cy.get(comboBoxSelector).type(`${option}{downArrow}{enter}`);
if (index === 0) {
// If we're filling a combobox that only allows a single value, there will be no "selections" to assert upon
cy.get(comboBoxSelector).should('contain', option);
} else {
cy.get(comboBoxSelectionsSelector).eq(index).should('have.text', option);
}
});
};