diff --git a/cypress/e2e/ui/Settings/Application-Settings/schedule.cy.js b/cypress/e2e/ui/Settings/Application-Settings/schedule.cy.js new file mode 100644 index 00000000000..2503af6b5de --- /dev/null +++ b/cypress/e2e/ui/Settings/Application-Settings/schedule.cy.js @@ -0,0 +1,489 @@ +/* eslint-disable no-undef */ + +const textConstants = { + // List items + schedulesAccordionItem: 'Schedules', + + // Field values + initialScheduleName: 'Test-name', + editedScheduleName: 'Dummy-name', + initialDescription: 'Test description', + editedDescription: 'Dummy description', + actionTypeVmAnalysis: 'vm', + actionTypeTemplateAnalysis: 'miq_template', + actionTypeHostAnalysis: 'host', + actionTypeContainerAnalysis: 'container_image', + actionTypeClusterAnalysis: 'emscluster', + actionTypeDataStoreAnalysis: 'storage', + actionTypeVmCompilanceCheck: 'vm_check_compliance', + actionTypeHostCompilanceCheck: 'host_check_compliance', + actionTypeContainerCompilanceCheck: 'container_image_check_compliance', + actionTypeAutomationTasks: 'automation_request', + filterTypeVmCluster: 'cluster', + timerTypeOnce: 'Once', + timerTypeHourly: 'Hourly', + timerTypeDaily: 'Daily', + timerTypeWeekly: 'Weekly', + timerTypeMonthly: 'Monthly', + frequencyTypeHour: '1 Hour', + timezoneTypeHawaii: '(GMT-10:00) Hawaii', + initialStartDate: '06/30/2025', + editedStartDate: '07/21/2025', + startTime: '11:23', + + // Buttons + saveButton: 'Save', + cancelButton: 'Cancel', + resetButton: 'Reset', + + // Config options + configToolbarButton: 'Configuration', + addScheduleConfigOption: 'Add a new Schedule', + deleteScheduleConfigOption: 'Delete this Schedule from the Database', + editScheduleConfigOption: 'Edit this Schedule', + disableScheduleConfigOption: 'Disable this Schedule', + enableScheduleConfigOption: 'Enable this Schedule', + queueScheduleConfigOption: 'Queue up this Schedule to run now', + + // Menu options + settingsMenuOption: 'Settings', + appSettingsMenuOption: 'Application Settings', + + // Flash message types + flashTypeSuccess: 'success', + flashTypeWarning: 'warning', + flashTypeError: 'error', + flashTypeInfo: 'info', + + // Flash message text snippets + flashMessageScheduleQueued: 'queued to run', + flashMessageOperationCanceled: 'cancelled', + flashMessageScheduleDisabled: 'disabled', + flashMessageScheduleEnabled: 'enabled', + flashMessageScheduleSaved: 'saved', + flashMessageResetSchedule: 'reset', + flashMessageScheduleDeleted: 'delete successful', + flashMessageFailedToAddSchedule: 'failed', + + // Browser alert text snippets + browserAlertDeleteConfirmText: 'will be permanently removed', +}; + +const { + settingsMenuOption, + appSettingsMenuOption, + actionTypeVmAnalysis, + actionTypeTemplateAnalysis, + actionTypeHostAnalysis, + actionTypeContainerAnalysis, + actionTypeClusterAnalysis, + actionTypeDataStoreAnalysis, + actionTypeVmCompilanceCheck, + actionTypeHostCompilanceCheck, + actionTypeContainerCompilanceCheck, + actionTypeAutomationTasks, + timerTypeOnce, + timerTypeHourly, + timerTypeDaily, + timerTypeWeekly, + timerTypeMonthly, + cancelButton, + saveButton, + initialScheduleName, + editScheduleConfigOption, + editedScheduleName, + editedDescription, + editedStartDate, + resetButton, + initialDescription, + initialStartDate, + disableScheduleConfigOption, + enableScheduleConfigOption, + queueScheduleConfigOption, + addScheduleConfigOption, + frequencyTypeHour, + timezoneTypeHawaii, + startTime, + deleteScheduleConfigOption, + schedulesAccordionItem, + configToolbarButton, + flashTypeSuccess, + flashTypeWarning, + flashTypeError, + flashTypeInfo, + flashMessageScheduleQueued, + flashMessageOperationCanceled, + flashMessageScheduleDisabled, + flashMessageScheduleEnabled, + flashMessageScheduleSaved, + flashMessageResetSchedule, + flashMessageScheduleDeleted, + flashMessageFailedToAddSchedule, + browserAlertDeleteConfirmText, +} = textConstants; + +function selectConfigMenu(menuOption = addScheduleConfigOption) { + return cy.toolbar(configToolbarButton, menuOption); +} + +function addSchedule() { + selectConfigMenu(); + // Checks if Save button is disabled initially + cy.contains( + '#main-content .bx--btn-set button[type="submit"]', + saveButton + ).should('be.disabled'); + // Adding data + cy.get('input#name').type(initialScheduleName); + cy.get('input#description').type(initialDescription); + cy.get('input[type="checkbox"]#enabled').check({ force: true }); + // Select Action type option: 'VM Analysis' + cy.get('select#action_typ').select(actionTypeVmAnalysis); + // Select Filter type option: 'A Single VM' + cy.get('select#filter_typ').select(actionTypeVmAnalysis); + // Select Run option: 'Hours' + cy.get('select#timer_typ').select(timerTypeHourly); + // Select Every option: '1 Hour' + cy.get('select#timer_value').select(frequencyTypeHour); + // Select Time zone option: '(GMT-10:00) Hawaii' + cy.get('input[role="combobox"]#time_zone').click(); + cy.contains('[role="option"]', timezoneTypeHawaii) + .should('be.visible') + .click(); + cy.get('input#start_date').type(initialStartDate); + cy.get('input#start_time').type(startTime); + // Intercepting the API call for adding a new schedule + cy.intercept('POST', '/ops/schedule_edit/new?button=save').as( + 'addScheduleApi' + ); + cy.contains('#main-content .bx--btn-set button[type="submit"]', saveButton) + .should('be.enabled') // Checks if Save button is enabled once all required fields are filled + .click(); + // Wait for the API call to complete + cy.wait('@addScheduleApi'); +} + +function deleteSchedule(scheduleName = initialScheduleName) { + // Selecting the schedule and intercepting the API call to get schedule details + interceptGetScheduleDetailsApi(scheduleName); + // Listening for the browser confirm alert and confirming deletion + cy.expect_browser_confirm_with_text({ + confirmTriggerFn: () => selectConfigMenu(deleteScheduleConfigOption), + containsText: browserAlertDeleteConfirmText, + }); + cy.expect_flash(flashTypeSuccess, flashMessageScheduleDeleted); +} + +function interceptGetScheduleDetailsApi(scheduleName = initialScheduleName) { + // Flag to check if the request is fired + let requestFired = false; + // Intercepting the API call + cy.intercept( + { + method: 'POST', + pathname: '/ops/tree_select', + query: { text: scheduleName }, + }, + // This callback function will be called when the request is fired, + // from which the requestFired flag will be set to true + () => (requestFired = true) + ).as('getCreatedScheduleApi'); + // Triggering the action that will fire the API call, + // which is selecting the created schedule + cy.accordionItem(scheduleName); + // Wait for the API call to complete if it was fired + // This is to ensure that the test does not fail if the request was not fired + cy.then(() => { + // If the request was fired, wait for the alias + if (requestFired) { + cy.wait('@getCreatedScheduleApi'); + } + }); +} + +function invokeCleanupDeletion() { + // Iterate and clean up any leftover schedules created during the test + cy.get('li.list-group-item').each(($el) => { + const text = $el?.text()?.trim(); + if (text === initialScheduleName) { + deleteSchedule(); + return false; + } + if (text === editedScheduleName) { + deleteSchedule(editedScheduleName); + return false; + } + return true; + }); +} + +function verifyFilterTypeDropdownExists() { + cy.get('label[for="filter_typ"]').should('exist'); + cy.get('select#filter_typ').should('exist'); +} + +function verifyTimerDropdownExists() { + cy.get('label[for="timer_value"]').should('exist'); + cy.get('select#timer_value').should('exist'); +} + +describe('Automate Schedule form operations: Settings > Application Settings > Settings > Schedules > Configuration > Add a new schedule', () => { + beforeEach(() => { + cy.login(); + cy.menu(settingsMenuOption, appSettingsMenuOption); + cy.intercept( + { + method: 'POST', + pathname: '/ops/tree_select', + query: { text: schedulesAccordionItem }, + }, + ).as('getSchedules'); + cy.accordionItem(schedulesAccordionItem); + cy.wait('@getSchedules'); + }); + + it('Validate visibility of elements based on dropdown selections', () => { + selectConfigMenu(); + + /* ===== Selecting any option other than "Automation Tasks" from "Action" dropdown does not hide the Filter dropdown ===== */ + + cy.get('select#action_typ').select(actionTypeVmAnalysis); + cy.get('select#action_typ').should('have.value', actionTypeVmAnalysis); + // Checking for Filter type dropdown + verifyFilterTypeDropdownExists(); + + cy.get('select#action_typ').select(actionTypeTemplateAnalysis); + cy.get('select#action_typ').should( + 'have.value', + actionTypeTemplateAnalysis + ); + // Checking for Filter type dropdown + verifyFilterTypeDropdownExists(); + + cy.get('select#action_typ').select(actionTypeHostAnalysis); + cy.get('select#action_typ').should('have.value', actionTypeHostAnalysis); + // Checking for Filter type dropdown + verifyFilterTypeDropdownExists(); + + cy.get('select#action_typ').select(actionTypeContainerAnalysis); + cy.get('select#action_typ').should( + 'have.value', + actionTypeContainerAnalysis + ); + // Checking for Filter type dropdown + verifyFilterTypeDropdownExists(); + + cy.get('select#action_typ').select(actionTypeClusterAnalysis); + cy.get('select#action_typ').should('have.value', actionTypeClusterAnalysis); + // Checking for Filter type dropdown + verifyFilterTypeDropdownExists(); + + cy.get('select#action_typ').select(actionTypeDataStoreAnalysis); + cy.get('select#action_typ').should( + 'have.value', + actionTypeDataStoreAnalysis + ); + // Checking for Filter type dropdown + verifyFilterTypeDropdownExists(); + + cy.get('select#action_typ').select(actionTypeVmCompilanceCheck); + cy.get('select#action_typ').should( + 'have.value', + actionTypeVmCompilanceCheck + ); + // Checking for Filter type dropdown + verifyFilterTypeDropdownExists(); + + cy.get('select#action_typ').select(actionTypeHostCompilanceCheck); + cy.get('select#action_typ').should( + 'have.value', + actionTypeHostCompilanceCheck + ); + // Checking for Filter type dropdown + verifyFilterTypeDropdownExists(); + + cy.get('select#action_typ').select(actionTypeContainerCompilanceCheck); + cy.get('select#action_typ').should( + 'have.value', + actionTypeContainerCompilanceCheck + ); + // Checking for Filter type dropdown + verifyFilterTypeDropdownExists(); + + /* ===== Selecting "Automation Tasks" option from "Action" dropdown shows Zone, Object details & Object fields ===== */ + + cy.get('select#action_typ').select(actionTypeAutomationTasks); + cy.get('select#action_typ').should('have.value', actionTypeAutomationTasks); + + // Checking for Zone dropdown + cy.get('label[for="zone_id"]').should('exist'); + cy.get('select#zone_id').should('exist'); + + // Checking for Object Details + cy.get('h3[name="object_details"]').should('exist'); + // Checking for System/Process dropdown + cy.get('label[for="instance_name"]').should('exist'); + cy.get('select#instance_name').should('exist'); + // Checking for Messsage textfield + cy.get('label[for="message"]').should('exist'); + cy.get('input#message').should('exist'); + // Checking for Request textfield + cy.get('label[for="request"]').should('exist'); + cy.get('input#request').should('exist'); + + // Checking for Object + cy.get('h3[name="object_attributes"]').should('exist'); + // Checking for Type Combobox + cy.get('label[for="target_class"]').should('exist'); + cy.get('input[role="combobox"]#target_class').should('exist'); + // Checking for Object Combobox + cy.get('label[for="target_id"]').should('exist'); + cy.get('input[role="combobox"]#target_id').should('exist'); + + // Checking for Attribute/Value pairs + cy.contains('h3', 'Attribute/Value Pairs').should('exist'); + // Checking for 5 attribute-value pairs text fields + cy.get('input#attribute_1').should('exist'); + cy.get('input#value_1').should('exist'); + cy.get('input#attribute_2').should('exist'); + cy.get('input#value_2').should('exist'); + cy.get('input#attribute_3').should('exist'); + cy.get('input#value_3').should('exist'); + cy.get('input#attribute_4').should('exist'); + cy.get('input#value_4').should('exist'); + cy.get('input#attribute_5').should('exist'); + cy.get('input#value_5').should('exist'); + + /* ===== Selecting "Once" option from "Run" dropdown does not show the "Every" dropdown ===== */ + + cy.get('select#timer_typ').select(timerTypeOnce); + // Checking whether the Every dropdown is hidden + cy.get('input#timer_value').should('not.exist'); + + /* ===== Selecting any other option other than "Once" from "Run" dropdown shows the "Every" dropdown ===== */ + + cy.get('select#timer_typ').select(timerTypeHourly); + // Checking whether the "Every" dropdown exist + verifyTimerDropdownExists(); + + cy.get('select#timer_typ').select(timerTypeDaily); + // Checking whether the "Every" dropdown exist + verifyTimerDropdownExists(); + + cy.get('select#timer_typ').select(timerTypeWeekly); + // Checking whether the "Every" dropdown exist + verifyTimerDropdownExists(); + + cy.get('select#timer_typ').select(timerTypeMonthly); + // Checking whether the "Every" dropdown exist + verifyTimerDropdownExists(); + }); + + it('Checking whether Cancel button works on the Add form', () => { + selectConfigMenu(); + cy.contains( + '#main-content .bx--btn-set button[type="button"]', + cancelButton + ) + .should('be.enabled') + .click(); + cy.expect_flash(flashTypeSuccess, flashMessageOperationCanceled); + }); + + it('Checking whether add, edit & delete schedule works', () => { + /* ===== Adding a schedule ===== */ + addSchedule(); + cy.expect_flash(flashTypeSuccess, flashMessageScheduleSaved); + + /* ===== Editing a schedule ===== */ + // Selecting the schedule and intercepting the API call to get schedule details + interceptGetScheduleDetailsApi(); + selectConfigMenu(editScheduleConfigOption); + // Editing name and description + cy.get('input#name').clear().type(editedScheduleName); + cy.get('input#description').clear().type(editedDescription); + // Confirms Save button is enabled after making edits + cy.contains('#main-content .bx--btn-set button[type="submit"]', saveButton) + .should('be.enabled') + .click(); + cy.expect_flash(flashTypeSuccess, flashMessageScheduleSaved); + + /* ===== Delete is already handled from afterEach hook ===== */ + }); + + it('Checking whether Cancel & Reset buttons work fine in the Edit form', () => { + /* ===== Adding a schedule ===== */ + addSchedule(); + + /* ===== Checking whether Cancel button works ===== */ + // Selecting the schedule and intercepting the API call to get schedule details + interceptGetScheduleDetailsApi(); + selectConfigMenu(editScheduleConfigOption); + cy.contains( + '#main-content .bx--btn-set button[type="button"]', + cancelButton + ) + .should('be.enabled') + .click(); + cy.expect_flash(flashTypeSuccess, flashMessageOperationCanceled); + + /* ===== Checking whether Reset button works ===== */ + // Selecting the schedule and intercepting the API call to get schedule details + interceptGetScheduleDetailsApi(); + selectConfigMenu(editScheduleConfigOption); + // Editing description and start date + cy.get('input#description').clear().type(editedDescription); + cy.get('input#start_date').clear().type(editedStartDate); + cy.contains('#main-content .bx--btn-set button[type="button"]', resetButton) + .should('be.enabled') + .click(); + cy.expect_flash(flashTypeWarning, flashMessageResetSchedule); + // Confirming the edited fields contain the old values after resetting + cy.get('input#description').should('have.value', initialDescription); + cy.get('input#start_date').should('have.value', initialStartDate); + + // Selecting Schedules menu item to bypass a bug, can be removed once #9505 is merged + cy.accordionItem(schedulesAccordionItem); + }); + + it('Checking whether creating a duplicate record is restricted', () => { + /* ===== Adding schedule ===== */ + addSchedule(); + + /* ===== Trying to add the same schedule again ===== */ + addSchedule(); + cy.expect_flash(flashTypeError, flashMessageFailedToAddSchedule); + }); + + it('Checking whether Disabling, Enabling & Queueing up the schedule works', () => { + /* ===== Adding a schedule ===== */ + addSchedule(); + // Selecting the schedule and intercepting the API call to get schedule details + interceptGetScheduleDetailsApi(); + + /* ===== Disabling the schedule ===== */ + selectConfigMenu(disableScheduleConfigOption); + cy.expect_flash(flashTypeInfo, flashMessageScheduleDisabled); + + /* ===== Enabling the schedule ===== */ + selectConfigMenu(enableScheduleConfigOption); + cy.expect_flash(flashTypeInfo, flashMessageScheduleEnabled); + + /* ===== Queueing-up the schedule ===== */ + selectConfigMenu(queueScheduleConfigOption); + cy.expect_flash(flashTypeSuccess, flashMessageScheduleQueued); + }); + + afterEach(() => { + cy?.url()?.then((url) => { + // Ensures navigation to Settings -> Application-Settings in the UI + if (url?.includes('/ops/explorer')) { + invokeCleanupDeletion(); + } else { + // Navigate to Settings -> Application-Settings before looking out for Schedules created during test + cy.menu(settingsMenuOption, appSettingsMenuOption); + invokeCleanupDeletion(); + } + }); + }); +}); diff --git a/cypress/support/assertions/expect_alerts.js b/cypress/support/assertions/expect_alerts.js new file mode 100644 index 00000000000..938299a290b --- /dev/null +++ b/cypress/support/assertions/expect_alerts.js @@ -0,0 +1,73 @@ +/* eslint-disable no-undef */ + +const flashClassMap = { + warning: 'warning', + error: 'danger', + info: 'info', + success: 'success', +}; + +/** + * Custom Cypress command to validate flash messages. + * @param {string} flashType - Type of flash (success, warning, error, info). + * @param {string} [containsText] - Optional text that the flash-message should contain. + * @returns {Cypress.Chainable} - The flash-message element if found, or an assertion failure. + */ +Cypress.Commands.add( + 'expect_flash', + (flashType = flashClassMap.success, containsText) => { + const flashMessageClassName = flashClassMap[flashType] || flashClassMap.success; + const flashMessageElement = cy + .get(`#main_div #flash_msg_div .alert-${flashMessageClassName}`) + .should('be.visible'); + + if (containsText) { + return flashMessageElement.should(($el) => { + const actualText = $el.text().toLowerCase(); + expect(actualText).to.include(containsText.toLowerCase()); + }); + } + + return flashMessageElement; + } +); + +/** + * Custom Cypress command to validate browser confirm alerts. + * @param {Object} options - Options for the command. + * @param {Function} options.confirmTriggerFn - A function that triggers the confirm dialog. + * This function **must return a Cypress.Chainable**, like `cy.get(...).click()`, + * so that Cypress can properly wait and chain `.then()` afterward. + * @example + * cy.expectBrowserConfirm({ + * containsText: 'sure to proceed?', + * proceed: true, + * confirmTriggerFn: () => { + * return cy.get('[data-testid="delete"]').click() + * } + * }); + * @example + * cy.expectBrowserConfirm({ + * confirmTriggerFn: () => cy.contains('deleted').click() + * }); + * @param {string} [options.containsText] - Optional text that the confirm alert should contain. + * @param {boolean} [options.proceed=true] - Whether to proceed with the confirm (true = OK, false = Cancel). + */ +Cypress.Commands.add( + 'expect_browser_confirm_with_text', + ({ confirmTriggerFn, containsText, proceed = true }) => { + let alertTriggered = false; + cy.on('window:confirm', (actualText) => { + alertTriggered = true; + if (containsText) { + expect(actualText.toLowerCase()).to.include(containsText.toLowerCase()); + } + return proceed; // true = OK, false = Cancel + }); + // Fires the event that triggers the confirm dialog + confirmTriggerFn().then(() => { + expect(alertTriggered, 'Expected browser confirm alert to be triggered') + .to.be.true; + }); + } +); diff --git a/cypress/support/commands/toolbar.js b/cypress/support/commands/toolbar.js index d5a09520835..6f98bf425f0 100644 --- a/cypress/support/commands/toolbar.js +++ b/cypress/support/commands/toolbar.js @@ -15,18 +15,22 @@ Cypress.Commands.add('toolbar', (toolbarButton, dropdownButton = '') => { }); if (dropdownButton) { - cy.get('.bx--overflow-menu-options').then((dropdownButtons) => { + return cy.get('.bx--overflow-menu-options').then((dropdownButtons) => { const buttons = dropdownButtons.children(); - const nums = [...Array(buttons.length).keys()]; - nums.forEach((index) => { + for (let index = 0; index < buttons.length; index++) { const button = buttons[index]; - if (button && button.innerText && button.innerText.includes(dropdownButton)) { - button.children[0].click(); - return; + if ( + button && + button.innerText && + button.innerText.includes(dropdownButton) + ) { + return cy.wrap(button.children[0]).click(); } - }); + } + return cy.wrap(null); }); } + return cy.wrap(null); }); // toolbarButton: String for the text of the toolbar button to click. diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index b3daa059bc8..1449a55f3ad 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -53,13 +53,15 @@ import './assertions/expect_rates_table.js'; import './assertions/expect_search_box.js' import './assertions/expect_title.js' import './assertions/expect_text.js' +import './assertions/expect_alerts.js'; // This is needed to prevent Cypress tests from failing due to uncaught errors: // Undefined errors are occuring on every initial page load of Manage IQ // Network and aborted errors are exlusive to firefox when cypress navigates to a new page before the api calls for the last page are fullly loaded Cypress.on('uncaught:exception', (err, runnable) => { console.log(err.message); - if (err.message.includes(`Cannot read properties of undefined (reading 'received')`) || // Error handler for Chrome + if (err.message.includes(`Cannot read properties of undefined (reading 'received')`) || // Error handler for Chrome + err.message.includes(`Cannot read properties of undefined (reading '0')`) || // Error handler for Chrome err.message.includes('subscription is undefined') || // Error handler for Firefox err.message.includes('NetworkError when attempting to fetch resource.') || // Error handler for Firefox err.message.includes('The operation was aborted.')) // Error handler for Firefox