diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx
new file mode 100644
index 00000000000000..84779ccb91b00e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx
@@ -0,0 +1,332 @@
+/*
+ * 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 React from 'react';
+import { EuiThemeProvider } from '@elastic/eui';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { uniq, sortBy, isEqual } from 'lodash';
+
+import { RuleDiffTab } from '../rule_diff_tab';
+import { savedRuleMock } from '../../../logic/mock';
+import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_schemas.gen';
+import { COLORS } from './constants';
+
+/*
+ Finds an element with a text content that exactly matches the passed argument.
+ Handly because React Testing Library's doesn't provide an easy way to search by
+ text if the text is split into multiple DOM elements.
+*/
+function findChildByTextContent(parent: Element, textContent: string): HTMLElement {
+ return Array.from(parent.querySelectorAll('*')).find(
+ (childElement) => childElement.textContent === textContent
+ ) as HTMLElement;
+}
+
+/*
+ Finds a diff line element (".diff-line") that contains a particular text content.
+ Match doesn't have to be exact, it's enough for the line to include the text.
+*/
+function findDiffLineContaining(text: string): Element | null {
+ const foundLine = Array.from(document.querySelectorAll('.diff-line')).find((element) =>
+ (element.textContent || '').includes(text)
+ );
+
+ return foundLine || null;
+}
+
+describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () => {
+ it.each(['light', 'dark'] as const)(
+ 'User can see precisely how property values would change after upgrade - %s theme',
+ (colorMode) => {
+ const oldRule: RuleResponse = {
+ ...savedRuleMock,
+ };
+
+ const newRule: RuleResponse = {
+ ...savedRuleMock,
+ };
+
+ /* Changes to test line update */
+ oldRule.version = 1;
+ newRule.version = 2;
+
+ /* Changes to test line removal */
+ oldRule.author = ['Alice', 'Bob', 'Charlie'];
+ newRule.author = ['Alice', 'Charlie'];
+
+ /* Changes to test line addition */
+ delete oldRule.license;
+ newRule.license = 'GPLv3';
+
+ const ThemeWrapper: React.FC<{}> = ({ children }) => (
+ {children}
+ );
+
+ const { container } = render(, {
+ wrapper: ThemeWrapper,
+ });
+
+ /* LINE UPDATE */
+ const updatedLine = findChildByTextContent(container, '- "version": 1+ "version": 2');
+
+ const updatedLineBefore = findChildByTextContent(updatedLine, ' "version": 1');
+ expect(updatedLineBefore).toHaveStyle(
+ `background: ${COLORS[colorMode].lineBackground.deletion}`
+ );
+
+ const updatedWordBefore = findChildByTextContent(updatedLineBefore, '1');
+ expect(updatedWordBefore).toHaveStyle(
+ `background: ${COLORS[colorMode].characterBackground.deletion}`
+ );
+
+ const updatedLineAfter = findChildByTextContent(updatedLine, ' "version": 2');
+ expect(updatedLineAfter).toHaveStyle(
+ `background: ${COLORS[colorMode].lineBackground.insertion}`
+ );
+
+ const updatedWordAfter = findChildByTextContent(updatedLineAfter, '2');
+ expect(updatedWordAfter).toHaveStyle(
+ `background: ${COLORS[colorMode].characterBackground.insertion}`
+ );
+
+ /* LINE REMOVAL */
+ const removedLine = findChildByTextContent(container, '- "Bob",');
+
+ const removedLineBefore = findChildByTextContent(removedLine, ' "Bob",');
+ expect(removedLineBefore).toHaveStyle(
+ `background: ${COLORS[colorMode].lineBackground.deletion}`
+ );
+
+ const removedLineAfter = findChildByTextContent(removedLine, '');
+ expect(window.getComputedStyle(removedLineAfter).backgroundColor).toBe('');
+
+ /* LINE ADDITION */
+ const addedLine = findChildByTextContent(container, '+ "license": "GPLv3",');
+
+ const addedLineBefore = findChildByTextContent(addedLine, '');
+ expect(window.getComputedStyle(addedLineBefore).backgroundColor).toBe('');
+
+ const addedLineAfter = findChildByTextContent(addedLine, ' "license": "GPLv3",');
+ expect(addedLineAfter).toHaveStyle(
+ `background: ${COLORS[colorMode].lineBackground.insertion}`
+ );
+ }
+ );
+
+ it('Rule actions and exception lists should not be shown as modified', () => {
+ const testAction = {
+ group: 'default',
+ id: 'my-action-id',
+ params: { body: '{"test": true}' },
+ action_type_id: '.webhook',
+ uuid: '1ef8f105-7d0d-434c-9ba1-2e053edddea8',
+ frequency: {
+ summary: true,
+ notifyWhen: 'onActiveAlert',
+ throttle: null,
+ },
+ } as const;
+
+ const testExceptionListItem = {
+ id: 'acbbbd86-7973-40a4-bc83-9e926c7f1e59',
+ list_id: '1e51e9b9-b7c0-4a11-8785-55f740b9938a',
+ type: 'rule_default',
+ namespace_type: 'single',
+ } as const;
+
+ const oldRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 1,
+ actions: [testAction],
+ };
+
+ const newRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 2,
+ };
+
+ /* Case: rule update doesn't have "actions" or "exception_list" properties */
+ const { rerender } = render();
+ expect(screen.queryAllByText('"actions":', { exact: false })).toHaveLength(0);
+
+ /* Case: rule update has "actions" and "exception_list" equal to empty arrays */
+ rerender(
+
+ );
+ expect(screen.queryAllByText('"actions":', { exact: false })).toHaveLength(0);
+
+ /* Case: rule update has an action and an exception list item */
+ rerender(
+
+ );
+ expect(screen.queryAllByText('"actions":', { exact: false })).toHaveLength(0);
+ });
+
+ describe('Technical properties should not be included in preview', () => {
+ it.each(['revision', 'created_at', 'created_by', 'updated_at', 'updated_by'])(
+ 'Should not include "%s" in preview',
+ (property) => {
+ const oldRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 1,
+ revision: 100,
+ created_at: '12/31/2023T23:59:000z',
+ created_by: 'mockUserOne',
+ updated_at: '01/01/2024T00:00:000z',
+ updated_by: 'mockUserTwo',
+ };
+
+ const newRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 2,
+ revision: 1,
+ created_at: '12/31/2023T23:59:999z',
+ created_by: 'mockUserOne',
+ updated_at: '02/02/2024T00:00:001z',
+ updated_by: 'mockUserThree',
+ };
+
+ render();
+ expect(screen.queryAllByText(property, { exact: false })).toHaveLength(0);
+ }
+ );
+ });
+
+ it('Properties with semantically equal values should not be shown as modified', () => {
+ const oldRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 1,
+ };
+
+ const newRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 2,
+ };
+
+ /* DURATION */
+ /* Semantically equal durations should not be shown as modified */
+ const { rerender } = render(
+
+ );
+ expect(findDiffLineContaining('"from":')).toBeNull();
+
+ rerender(
+
+ );
+ expect(findDiffLineContaining('"from":')).toBeNull();
+
+ rerender(
+
+ );
+ expect(findDiffLineContaining('"from":')).toBeNull();
+
+ /* Semantically different durations should generate diff */
+ rerender(
+
+ );
+ expect(findDiffLineContaining('- "from": "now-7260s",+ "from": "now-7200s",')).not.toBeNull();
+
+ /* NOTE - Investigation guide */
+ rerender();
+ expect(findDiffLineContaining('"note":')).toBeNull();
+
+ rerender(
+
+ );
+ expect(findDiffLineContaining('"note":')).toBeNull();
+
+ rerender();
+ expect(findDiffLineContaining('"note":')).toBeNull();
+
+ rerender();
+ expect(findDiffLineContaining('- "note": "",+ "note": "abc",')).not.toBeNull();
+ });
+
+ it('Unchanged sections of a rule should be hidden by default', () => {
+ const oldRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 1,
+ };
+
+ const newRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 2,
+ };
+
+ render();
+ expect(screen.queryAllByText('"author":', { exact: false })).toHaveLength(0);
+ expect(screen.queryAllByText('Expand 44 unchanged lines')).toHaveLength(1);
+
+ userEvent.click(screen.getByText('Expand 44 unchanged lines'));
+
+ expect(screen.queryAllByText('Expand 44 unchanged lines')).toHaveLength(0);
+ expect(screen.queryAllByText('"author":', { exact: false })).toHaveLength(2);
+ });
+
+ it('Properties should be sorted alphabetically', () => {
+ const oldRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 1,
+ };
+
+ const newRule: RuleResponse = {
+ ...savedRuleMock,
+ version: 2,
+ };
+
+ function checkRenderedPropertyNamesAreSorted(): boolean {
+ /* Find all lines which contain property names in the diff */
+ const matchedElements = screen.queryAllByText(/\s".*?":/, { trim: false });
+
+ /* Extract property names from the matched elements */
+ const propertyNames = matchedElements.map((element) => {
+ const matches = element.textContent?.match(/\s"(.*?)":/);
+ return matches ? matches[1] : '';
+ });
+
+ /* Remove duplicates */
+ const uniquePropertyNames = uniq(propertyNames);
+
+ /* Check that displayed property names are sorted alphabetically */
+ const isArraySortedAlphabetically = (array: string[]): boolean =>
+ isEqual(array, sortBy(array));
+
+ return isArraySortedAlphabetically(uniquePropertyNames);
+ }
+
+ render();
+ const arePropertiesSortedInConciseView = checkRenderedPropertyNamesAreSorted();
+ expect(arePropertiesSortedInConciseView).toBe(true);
+
+ userEvent.click(screen.getByText('Expand 44 unchanged lines'));
+ const arePropertiesSortedInExpandedView = checkRenderedPropertyNamesAreSorted();
+ expect(arePropertiesSortedInExpandedView).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx
index 52277d1cbad15a..b22fc1b73bbe6d 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx
@@ -48,6 +48,15 @@ const HIDDEN_PROPERTIES = [
and will therefore always show a diff. It adds no value to display it to the user.
*/
'updated_at',
+
+ /*
+ These values make sense only for installed prebuilt rules.
+ They are not present in the prebuilt rule package.
+ So, showing them in the diff doesn't add value.
+ */
+ 'updated_by',
+ 'created_at',
+ 'created_by',
];
const sortAndStringifyJson = (jsObject: Record): string =>
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts
index 855e2f554c5b6b..0cd75c6162be37 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts
@@ -9,6 +9,7 @@ import { omit } from 'lodash';
import type { Filter } from '@kbn/es-query';
import type { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
import type { PrebuiltRuleAsset } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules';
+import type { ReviewRuleUpgradeResponseBody } from '@kbn/security-solution-plugin/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route';
import type { Threshold } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema';
import { AlertSuppression } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema';
@@ -49,12 +50,14 @@ import {
assertMachineLearningPropertiesShown,
assertNewTermsFieldsPropertyShown,
assertSavedQueryPropertiesShown,
+ assertSelectedPreviewTab,
assertThreatMatchQueryPropertiesShown,
assertThresholdPropertyShown,
assertWindowSizePropertyShown,
closeRulePreview,
openRuleInstallPreview,
openRuleUpdatePreview,
+ selectPreviewTab,
} from '../../../../tasks/prebuilt_rules_preview';
import { visitRulesManagementTable } from '../../../../tasks/rules_management';
import {
@@ -62,9 +65,16 @@ import {
deleteDataView,
postDataView,
} from '../../../../tasks/api_calls/common';
+import { enableRules, waitForRulesToFinishExecution } from '../../../../tasks/api_calls/rules';
const TEST_ENV_TAGS = ['@ess', '@serverless'];
+const PREVIEW_TABS = {
+ OVERVIEW: 'Overview',
+ JSON_VIEW: 'JSON view',
+ UPDATES: 'Updates',
+};
+
describe('Detection rules, Prebuilt Rules Installation and Update workflow', () => {
const commonProperties: Partial = {
author: ['Elastic', 'Another author'],
@@ -662,7 +672,6 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
installPrebuiltRuleAssets([UPDATED_RULE_1, UPDATED_RULE_2]);
visitRulesManagementTable();
- clickRuleUpdatesTab();
});
describe('Basic functionality', { tags: TEST_ENV_TAGS }, () => {
@@ -842,6 +851,8 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
clickRuleUpdatesTab();
openRuleUpdatePreview(UPDATED_CUSTOM_QUERY_INDEX_PATTERN_RULE['security-rule'].name);
+ assertSelectedPreviewTab(PREVIEW_TABS.JSON_VIEW);
+ selectPreviewTab(PREVIEW_TABS.OVERVIEW);
const { index } = UPDATED_CUSTOM_QUERY_INDEX_PATTERN_RULE['security-rule'] as {
index: string[];
@@ -868,6 +879,8 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
closeRulePreview();
openRuleUpdatePreview(UPDATED_SAVED_QUERY_DATA_VIEW_RULE['security-rule'].name);
+ assertSelectedPreviewTab(PREVIEW_TABS.JSON_VIEW);
+ selectPreviewTab(PREVIEW_TABS.OVERVIEW);
const { data_view_id: dataViewId } = UPDATED_SAVED_QUERY_DATA_VIEW_RULE[
'security-rule'
@@ -889,6 +902,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
clickRuleUpdatesTab();
openRuleUpdatePreview(UPDATED_MACHINE_LEARNING_RULE['security-rule'].name);
+ selectPreviewTab(PREVIEW_TABS.OVERVIEW);
assertCommonPropertiesShown(commonProperties);
@@ -910,6 +924,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
clickRuleUpdatesTab();
openRuleUpdatePreview(UPDATED_THRESHOLD_RULE_INDEX_PATTERN['security-rule'].name);
+ selectPreviewTab(PREVIEW_TABS.OVERVIEW);
assertCommonPropertiesShown(commonProperties);
@@ -940,6 +955,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
clickRuleUpdatesTab();
openRuleUpdatePreview(UPDATED_EQL_INDEX_PATTERN_RULE['security-rule'].name);
+ selectPreviewTab(PREVIEW_TABS.OVERVIEW);
assertCommonPropertiesShown(commonProperties);
@@ -956,6 +972,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
clickRuleUpdatesTab();
openRuleUpdatePreview(UPDATED_THREAT_MATCH_INDEX_PATTERN_RULE['security-rule'].name);
+ selectPreviewTab(PREVIEW_TABS.OVERVIEW);
assertCommonPropertiesShown(commonProperties);
@@ -999,6 +1016,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
clickRuleUpdatesTab();
openRuleUpdatePreview(UPDATED_NEW_TERMS_INDEX_PATTERN_RULE['security-rule'].name);
+ selectPreviewTab(PREVIEW_TABS.OVERVIEW);
assertCommonPropertiesShown(commonProperties);
@@ -1040,6 +1058,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
clickRuleUpdatesTab();
openRuleUpdatePreview(UPDATED_ESQL_RULE['security-rule'].name);
+ selectPreviewTab(PREVIEW_TABS.OVERVIEW);
assertCommonPropertiesShown(commonProperties);
@@ -1049,5 +1068,73 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', ()
}
);
});
+
+ describe('Viewing rule changes in JSON diff view', { tags: TEST_ENV_TAGS }, () => {
+ it('User can see changes in a side-by-side JSON diff view', () => {
+ clickRuleUpdatesTab();
+
+ openRuleUpdatePreview(OUTDATED_RULE_1['security-rule'].name);
+ assertSelectedPreviewTab(PREVIEW_TABS.JSON_VIEW);
+
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW).contains('Current rule').should('be.visible');
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW).contains('Elastic update').should('be.visible');
+
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW).contains('"version": 1').should('be.visible');
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW).contains('"version": 2').should('be.visible');
+
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW)
+ .contains('"name": "Outdated rule 1"')
+ .should('be.visible');
+
+ /* Select another rule without closing the preview for the current rule */
+ openRuleUpdatePreview(OUTDATED_RULE_2['security-rule'].name);
+
+ /* Make sure the JSON diff is displayed for the newly selected rule */
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW)
+ .contains('"name": "Outdated rule 2"')
+ .should('be.visible');
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW)
+ .contains('"name": "Outdated rule 1"')
+ .should('not.exist');
+ });
+
+ it('Dynamic properties should not be included in preview', () => {
+ const dateBeforeRuleExecution = new Date();
+
+ /* Enable a rule and wait for it to execute */
+ enableRules({ names: [OUTDATED_RULE_1['security-rule'].name] });
+ waitForRulesToFinishExecution(
+ [OUTDATED_RULE_1['security-rule'].rule_id],
+ dateBeforeRuleExecution
+ );
+
+ cy.intercept('POST', '/internal/detection_engine/prebuilt_rules/upgrade/_review').as(
+ 'updatePrebuiltRulesReview'
+ );
+
+ clickRuleUpdatesTab();
+
+ /* Check that API response contains dynamic properties, like "enabled" and "execution_summary" */
+ cy.wait('@updatePrebuiltRulesReview')
+ .its('response.body')
+ .then((body: ReviewRuleUpgradeResponseBody) => {
+ const executedRuleInfo = body.rules.find(
+ (ruleInfo) => ruleInfo.rule_id === OUTDATED_RULE_1['security-rule'].rule_id
+ );
+
+ const enabled = executedRuleInfo?.current_rule?.enabled;
+ expect(enabled).to.eql(true);
+
+ const executionSummary = executedRuleInfo?.current_rule?.execution_summary;
+ expect(executionSummary).to.not.eql(undefined);
+ });
+
+ /* Open the preview and check that dynamic properties are not shown in the diff */
+ openRuleUpdatePreview(OUTDATED_RULE_1['security-rule'].name);
+
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW).contains('enabled').should('not.exist');
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW).contains('execution_summary').should('not.exist');
+ });
+ });
});
});
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/machine_learning.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/machine_learning.ts
index f03d6edadbc18f..3dc9fda54d35cb 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/machine_learning.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/machine_learning.ts
@@ -13,6 +13,9 @@ export const fetchMachineLearningModules = () => {
return rootRequest({
method: 'GET',
url: `${ML_INTERNAL_BASE_PATH}/modules/get_module`,
+ headers: {
+ 'elastic-api-version': '1',
+ },
failOnStatusCode: false,
});
};
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts
index 5f89e57ff81c60..d8b4c9f3bf7cb5 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts
@@ -7,6 +7,7 @@
import moment from 'moment';
import {
+ DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_URL,
DETECTION_ENGINE_RULES_URL_FIND,
} from '@kbn/security-solution-plugin/common/constants';
@@ -109,3 +110,28 @@ export const waitForRulesToFinishExecution = (ruleIds: string[], afterDate?: Dat
}),
{ interval: 500, timeout: 12000 }
);
+
+type EnableRulesParameters =
+ | {
+ names: string[];
+ ids?: undefined;
+ }
+ | {
+ names?: undefined;
+ ids: string[];
+ };
+
+export const enableRules = ({ names, ids }: EnableRulesParameters): Cypress.Chainable => {
+ const query = names?.map((name) => `alert.attributes.name: "${name}"`).join(' OR ');
+
+ return rootRequest({
+ method: 'POST',
+ url: DETECTION_ENGINE_RULES_BULK_ACTION,
+ body: {
+ action: 'enable',
+ query,
+ ids,
+ },
+ failOnStatusCode: false,
+ });
+};
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules_preview.ts b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules_preview.ts
index b3f0c1316827b8..f169bd98ee59ce 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules_preview.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules_preview.ts
@@ -117,6 +117,16 @@ export const closeRulePreview = () => {
cy.get(INSTALL_PREBUILT_RULE_PREVIEW).should('not.exist');
};
+export const selectPreviewTab = (tabTitle: string) =>
+ cy.get(UPDATE_PREBUILT_RULE_PREVIEW).find('.euiTab').contains(tabTitle).click();
+
+export const assertSelectedPreviewTab = (tabTitle: string) =>
+ cy
+ .get(UPDATE_PREBUILT_RULE_PREVIEW)
+ .find('.euiTab-isSelected')
+ .invoke('text')
+ .should('eq', tabTitle);
+
export const assertCommonPropertiesShown = (properties: Partial) => {
cy.get(AUTHOR_PROPERTY_TITLE).should('have.text', 'Author');
cy.get(AUTHOR_PROPERTY_VALUE_ITEM).then((items) => {