From cd374d23368de182a96df0948192a9bca7bdc4aa Mon Sep 17 00:00:00 2001 From: Nikita Indik Date: Mon, 19 Feb 2024 16:12:51 +0100 Subject: [PATCH] [Security Solution] JSON diffs test coverage (#176770) **Resolves: https://github.com/elastic/kibana/issues/166163** Flaky test runner runs: [1](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5189), [2](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5190), [3](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5191), [4](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5192) ## Summary This PR adds tests in accordance with the [test plan](https://github.com/elastic/kibana/pull/175958) that was merged earlier. --- .../rule_details/json_diff/json_diff.test.tsx | 332 ++++++++++++++++++ .../components/rule_details/rule_diff_tab.tsx | 9 + .../prebuilt_rules_preview.cy.ts | 89 ++++- .../tasks/api_calls/machine_learning.ts | 3 + .../cypress/tasks/api_calls/rules.ts | 26 ++ .../cypress/tasks/prebuilt_rules_preview.ts | 10 + 6 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx 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) => {