diff --git a/app/controllers/miq_ae_class_controller.rb b/app/controllers/miq_ae_class_controller.rb index bb7da5ba57c..0dcf61e33a9 100644 --- a/app/controllers/miq_ae_class_controller.rb +++ b/app/controllers/miq_ae_class_controller.rb @@ -131,7 +131,6 @@ def set_right_cell_text(id, rec = nil) txt = rec.domain? ? _('Automate Domain') : _('Automate Namespace') @sb[:namespace_path] = rec.fqname end - @sb[:namespace_path]&.gsub!(%r{\/}, " / ") @right_cell_text = "#{txt} #{_("\"%s\"") % get_rec_name(rec)}" unless %w[root aei aem].include?(nodes[0]) end @@ -341,6 +340,9 @@ def replace_right_cell(options = {}) :serialize => @sb[:active_tab] == 'methods', } ]) + if @hide_bottom_bar + presenter.hide(:paging_div, :form_buttons_div) + end else # incase it was hidden for summary screen, and incase there were no records on show_list presenter.hide(:paging_div, :form_buttons_div) @@ -459,6 +461,7 @@ def edit_class @ae_class = find_record_with_rbac(MiqAeClass, params[:id]) end set_form_vars + @hide_bottom_bar = true # have to get name and set node info, to load multiple tabs correctly # rec_name = get_rec_name(@ae_class) # get_node_info("aec-#{@ae_class.id}") @@ -468,6 +471,24 @@ def edit_class replace_right_cell end + def edit_class_record + assert_privileges("miq_ae_class_edit") + unless params[:id] + obj = find_checked_items + @_params[:id] = obj[0] + end + @hide_bottom_bar = true + + class_rec = MiqAeClass.find(params[:id]) + + render :json => { + :fqname => class_rec.fqname, + :name => class_rec.name, + :display_name => class_rec.display_name, + :description => class_rec.description + } + end + def edit_fields assert_privileges("miq_ae_field_edit") if params[:pressed] == "miq_ae_item_edit" # came from Namespace details screen @@ -1291,6 +1312,7 @@ def new assert_privileges("miq_ae_class_new") @ae_class = MiqAeClass.new set_form_vars + @hide_bottom_bar = true @in_a_form = true replace_right_cell end @@ -1841,8 +1863,50 @@ def ae_method_operations end end + def class_update + assert_privileges(params[:id].present? ? 'miq_ae_class_edit' : 'miq_ae_class_new') + @hide_bottom_bar = true + class_update_create + end + private + def class_update_create + case params[:button] + when "add", "save" + class_rec = params[:id].blank? ? MiqAeClass.new : MiqAeClass.find(params[:id]) # Get new or existing record + add_flash(_("Name is required"), :error) if params[:name].blank? + class_rec.name = params[:name] + class_rec.display_name = params[:display_name] + class_rec.description = params[:description] + class_rec.namespace_id = x_node.split('-')[1] if params[:id].blank? + begin + class_rec.save! + rescue StandardError + errors = [] + class_rec.errors.each do |error| + errors.push("#{error.attribute.to_s.capitalize} #{error.message}") + end + @changed = true + render :json => {:error => errors, :status => 500} + else + edit_hash = {} + edit_hash[:new] = {:name => params[:name], + :display_name => params[:display_name], :description => params[:description]} + edit_hash[:current] = if params[:old_data] + {:name => params[:old_data][:name], + :display_name => params[:old_data][:display_name], + :description => params[:old_data][:description]} + else + {:name => nil, :display_name => nil, :description => nil} + end + AuditEvent.success(build_saved_audit(class_rec, edit_hash)) + @edit = session[:edit] = nil # clean out the saved info + render :json => {:status => 200} + end + end + end + def get_template_class(location) if location == "ansible_workflow_template" ManageIQ::Providers::ExternalAutomationManager::ConfigurationWorkflow diff --git a/app/javascript/components/miq-ae-class/class-form.schema.js b/app/javascript/components/miq-ae-class/class-form.schema.js new file mode 100644 index 00000000000..dda2cd26785 --- /dev/null +++ b/app/javascript/components/miq-ae-class/class-form.schema.js @@ -0,0 +1,38 @@ +import { componentTypes } from '@@ddf'; + +const createSchema = (fqname) => ({ + fields: [ + { + component: componentTypes.TEXT_FIELD, + name: 'fqname', + label: 'Fully Qualified Name', + value: `${fqname}`, + disabled: true, + }, + { + component: componentTypes.TEXT_FIELD, + id: 'name', + name: 'name', + label: __('Name'), + maxLength: 128, + validate: [{ type: 'customValidatorForNameField' }], + isRequired: true, + }, + { + component: componentTypes.TEXT_FIELD, + id: 'display_name', + name: 'display_name', + label: __('Display Name'), + maxLength: 128, + }, + { + component: componentTypes.TEXT_FIELD, + id: 'description', + name: 'description', + label: __('Description'), + maxLength: 255, + }, + ], +}); + +export default createSchema; diff --git a/app/javascript/components/miq-ae-class/index.jsx b/app/javascript/components/miq-ae-class/index.jsx new file mode 100644 index 00000000000..7630e4aba08 --- /dev/null +++ b/app/javascript/components/miq-ae-class/index.jsx @@ -0,0 +1,193 @@ +import React, { useState, useEffect } from 'react'; +import { FormSpy } from '@data-driven-forms/react-form-renderer'; +import { Button } from 'carbon-components-react'; +import MiqFormRenderer, { useFormApi } from '@@ddf'; +import PropTypes from 'prop-types'; +import createSchema from './class-form.schema'; +import miqRedirectBack from '../../helpers/miq-redirect-back'; +import miqFlash from '../../helpers/miq-flash'; + +const MiqAeClass = ({ classRecord, fqname }) => { + const formattedFqname = fqname.replace(/\s+/g, ''); + const [data, setData] = useState({ + isLoading: true, + initialValues: undefined, + }); + + const isEdit = !!(classRecord && classRecord.id); + + useEffect(() => { + if (isEdit) { + http.get(`/miq_ae_class/edit_class_record/${classRecord.id}/`).then((recordValues) => { + if (recordValues) { + setData({ ...data, isLoading: false, initialValues: recordValues }); + } + }); + } else { + const initialValues = { + formattedFqname, + name: classRecord && classRecord.name, + display_name: classRecord && classRecord.display_name, + description: classRecord && classRecord.description, + }; + setData({ ...data, isLoading: false, initialValues }); + } + }, [classRecord]); + + const onSubmit = (values) => { + miqSparkleOn(); + + const params = { + action: isEdit ? 'edit' : 'create', + name: values.name, + display_name: values.display_name, + description: values.description, + old_data: data.initialValues, + button: classRecord.id ? 'save' : 'add', + }; + + const request = isEdit + ? http.post(`/miq_ae_class/class_update/${classRecord.id}`, params) + : http.post(`/miq_ae_class/class_update/`, params); + + request + .then((response) => { + if (response.status === 200) { + const confirmation = isEdit ? __(`Class "%s" was saved`) : __(`Class "%s" was added`); + const message = sprintf(confirmation, values.name); + miqRedirectBack(message, 'success', '/miq_ae_class/explorer'); + } else { + miqSparkleOff(); + miqFlash('error', response.error); + } + }) + .catch(miqSparkleOff); + }; + + const onCancel = () => { + const confirmation = classRecord.id ? __(`Edit of Class "%s" cancelled by the user`) + : __(`Add of new Class was cancelled by the user`); + const message = sprintf(confirmation, classRecord.name); + miqRedirectBack(message, 'warning', '/miq_ae_class/explorer'); + }; + + const customValidatorMapper = { + customValidatorForNameField: () => (value) => { + if (!value) { + return __('Required'); + } + if (!value.match('^[a-zA-Z0-9_.-]*$')) { + return __('Name may contain only alphanumeric and _ . - characters'); + } + return false; + }, + }; + + return (!data.isLoading + ? ( +
+ {}} + FormTemplate={(props) => } + /> +
+ ) : null + ); +}; + +const FormTemplate = ({ + formFields, recId, +}) => { + const { + handleSubmit, onReset, onCancel, getState, + } = useFormApi(); + const { valid, pristine } = getState(); + const submitLabel = !!recId ? __('Save') : __('Add'); + return ( +
+ {formFields} + + {() => ( +
+ { !recId + ? ( + + ) : ( + + )} + {!!recId + ? ( + + ) : null} + + +
+ )} +
+
+ ); +}; + +MiqAeClass.propTypes = { + classRecord: PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + display_name: PropTypes.string, + description: PropTypes.string, + }), + fqname: PropTypes.string.isRequired, +}; + +MiqAeClass.defaultProps = { + classRecord: undefined, +}; + +FormTemplate.propTypes = { + formFields: PropTypes.arrayOf( + PropTypes.shape({ id: PropTypes.number }), + PropTypes.shape({ name: PropTypes.string }), + PropTypes.shape({ display_name: PropTypes.string }), + PropTypes.shape({ description: PropTypes.string }), + ), + recId: PropTypes.number, +}; + +FormTemplate.defaultProps = { + formFields: undefined, + recId: undefined, +}; + +export default MiqAeClass; diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index f56709a0b77..b5e3b5de67f 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -177,6 +177,7 @@ import WorkflowPayload from '../components/workflows/workflow_payload'; import WorkflowRepositoryForm from '../components/workflow-repository-form'; import XmlHolder from '../components/XmlHolder'; import ZoneForm from '../components/zone-form'; +import MiqAeClass from '../components/miq-ae-class'; /** * Add component definitions to this file. @@ -363,3 +364,4 @@ ManageIQ.component.addReact('WorkflowPayload', WorkflowPayload); ManageIQ.component.addReact('WorkflowRepositoryForm', WorkflowRepositoryForm); ManageIQ.component.addReact('XmlHolder', XmlHolder); ManageIQ.component.addReact('ZoneForm', ZoneForm); +ManageIQ.component.addReact('MiqAeClass', MiqAeClass); diff --git a/app/javascript/spec/miq-ae-class-form/__snapshots__/miq-ae-class-form.spec.js.snap b/app/javascript/spec/miq-ae-class-form/__snapshots__/miq-ae-class-form.spec.js.snap new file mode 100644 index 00000000000..412578bfe67 --- /dev/null +++ b/app/javascript/spec/miq-ae-class-form/__snapshots__/miq-ae-class-form.spec.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MiqAeClass Form Component should render add class form correctly 1`] = `""`; + +exports[`MiqAeClass Form Component should render edit class form correctly 1`] = `""`; diff --git a/app/javascript/spec/miq-ae-class-form/miq-ae-class-form.spec.js b/app/javascript/spec/miq-ae-class-form/miq-ae-class-form.spec.js new file mode 100644 index 00000000000..8e369f9327b --- /dev/null +++ b/app/javascript/spec/miq-ae-class-form/miq-ae-class-form.spec.js @@ -0,0 +1,62 @@ +import React from 'react'; +import fetchMock from 'fetch-mock'; +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import MiqAeClass from '../../components/miq-ae-class'; + +describe('MiqAeClass Form Component', () => { + const classMockData = [ + { + href: `/miq_ae_class/edit_class/2/`, + id: 2, + description: 'Configured System Provision', + }, + ]; + + const MiqAeClassEditData = { + id: 40, + name: 'test', + display_name: 'test display name', + description: 'test description', + }; + + const fqName = 'Sample FQ Name'; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it('should render add class form correctly', async() => { + const wrapper = shallow(); + + fetchMock.get(`/miq_ae_class/new?&expand=resources/`, classMockData); + + await new Promise((resolve) => { + setImmediate(() => { + wrapper.update(); + expect(toJson(wrapper)).toMatchSnapshot(); + resolve(); + }); + }); + }); + + it('should render edit class form correctly', async() => { + const wrapper = shallow(); + + fetchMock.get(`/miq_ae_class/edit_class_react/${MiqAeClassEditData.id}?&expand=resources/`, classMockData); + await new Promise((resolve) => { + setImmediate(() => { + wrapper.update(); + expect(toJson(wrapper)).toMatchSnapshot(); + resolve(); + }); + }); + }); +}); diff --git a/app/views/miq_ae_class/_class_form.html.haml b/app/views/miq_ae_class/_class_form.html.haml index 5d3d9bc3808..a3e5041d203 100644 --- a/app/views/miq_ae_class/_class_form.html.haml +++ b/app/views/miq_ae_class/_class_form.html.haml @@ -1,35 +1,6 @@ -- url = url_for_only_path(:action => 'form_field_changed', :id => (@ae_class.id || 'new')) -- obs = {:interval => '.5', :url => url}.to_json -%h3 - = _('Properties') -.form-horizontal - .form-group - %label.col-md-2.control-label - = _('Fully Qualified Name') - .col-md-8 - = @sb[:namespace_path] - .form-group - %label.col-md-2.control-label - = _('Name') - .col-md-8 - = text_field_tag("name", @edit[:new][:name], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control", - "data-miq_observe" => obs) - = javascript_tag(javascript_focus('name')) - .form-group - %label.col-md-2.control-label - = _('Display Name') - .col-md-8 - = text_field_tag("display_name", @edit[:new][:display_name], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control", - "data-miq_observe" => obs) - .form-group - %label.col-md-2.control-label - = _('Description') - .col-md-8 - = text_field_tag("description", @edit[:new][:description], - :maxlength => ViewHelper::MAX_NAME_LEN, - :class => "form-control", - "data-miq_observe" => obs) +- if @ae_class + - if @in_a_form + = react('MiqAeClass', + :classRecord => @ae_class, + :fqname => @sb[:namespace_path]) + diff --git a/config/routes.rb b/config/routes.rb index 2056e137c7d..a796ad5f217 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1911,6 +1911,7 @@ ae_methods ae_method_operations show + edit_class_record ], :post => %w[ add_update_method @@ -1955,6 +1956,7 @@ x_button x_history x_show + class_update ] + adv_search_post + exp_post }, diff --git a/cypress/e2e/ui/Automation/Embedded-Automate/Explorer/class.cy.js b/cypress/e2e/ui/Automation/Embedded-Automate/Explorer/class.cy.js new file mode 100644 index 00000000000..ecdc1ed6f0a --- /dev/null +++ b/cypress/e2e/ui/Automation/Embedded-Automate/Explorer/class.cy.js @@ -0,0 +1,281 @@ +/* eslint-disable no-undef */ + +describe('Automation > Embedded Automate > Explorer', () => { + before(() => { + // Create a Domain and Namespace before all the tests + cy.login(); + cy.intercept('POST', '/ops/accordion_select?id=rbac_accord').as('accordion'); + cy.menu('Automation', 'Embedded Automate', 'Explorer'); + cy.get('#explorer_title_text'); + + // Creates a Domain + cy.get('[title="Datastore"]').click(); + cy.get('[title="Configuration"]').click(); + cy.get('[title="Add a New Domain"]').click(); + cy.get('[name="name"]').type('TestDomain', {force: true}); + cy.get('[name="description"]').type('This is a test domain'); + cy.get('#enabled').check(); + cy.get('[class="bx--btn bx--btn--primary"]').contains('Add').click(); + + // Check for the success message + cy.get('div.alert.alert-success.alert-dismissable').should('exist').and('contain', 'Automate Domain "TestDomain" was added').find('button.close').should('exist'); + + // Creates a Namespace + cy.get('[title="Datastore"]').click(); + cy.get('[title="Automate Domain: TestDomain"]').click(); // Click on Domain + cy.get('[title="Configuration"]').click(); + cy.get('[title="Add a New Namespace"]').click(); + cy.get('[name="name"]').type('TestNameSpace', {force: true}); + cy.get('[name="description"]').type('This is a test NameSpace'); + cy.get('.bx--btn--primary').contains('Add').click(); + + cy.wait(1000); // Need this wait or else namespace doesn't get added properly + }); + + beforeEach(() => { + cy.login(); + cy.intercept('POST', '/ops/accordion_select?id=rbac_accord').as('accordion'); + cy.menu('Automation', 'Embedded Automate', 'Explorer'); + cy.get('#explorer_title_text'); + }); + + describe('Class Form', () => { + it('Cancel button works on the form', () => { + // Clicks the Cancel button on the create class form + cy.get('[title="Datastore"]').click(); + cy.get('[title="Automate Domain: TestDomain"]').click(); + cy.get('[title="Automate Namespace: TestNameSpace"]').click(); + cy.get('[title="Configuration"]').click(); + cy.get('[title="Add a New Class"]').click(); + cy.get('[class="bx--btn bx--btn--secondary"]').contains('Cancel').click(); // clicks Cancel button + + // Make sure that the cancel button redirects back to the namespace page + cy.get('[id="explorer_title_text"]').contains('Automate Namespace "TestNameSpace"'); + cy.get('.flash_text_div > .alert').contains('Add of new Class was cancelled by the user'); + cy.get('#ns_details_div > .alert').contains('The selected Namespace is empty'); + }); + + it('Reset button works on the form', () => { + // Creates a Class + cy.get('[title="Datastore"]').click(); + cy.get('[title="Automate Domain: TestDomain"]').click(); // clicks on Domain + cy.get('[title="Automate Namespace: TestNameSpace"]').click(); // clicks on Namespace + cy.get('[title="Configuration"]').click(); + cy.get('[title="Add a New Class"]').click(); + cy.get('[name="name"]').type('TestClass', {force: true}); + cy.get('[name="display_name"]').type('Test Class 0'); + cy.get('[name="description"').type('This is a test class description'); + cy.get('.bx--btn--primary').contains('Add').click(); + + // Check for the success message + cy.get('#flash_msg_div .alert.alert-success').should('exist').and('be.visible').and('contain', 'Class "TestClass" was added'); + + // Navigate to the Properties tab of the class just created + cy.get('.clickable-row').contains('Test Class 0').click(); + cy.get('#props_tab > a').click(); + + // Edits the class + cy.get('[title="Configuration"]').click(); + cy.get('[title="Edit this Class"]').click(); + cy.get('[name="name"]').type('Edit', {force: true}); + cy.get('[name="display_name"]').clear().type('Edited Test Class'); + cy.get('[name="description"').clear().type('Edited test class description'); + + // Click Reset Button + cy.get('.btnRight.bx--btn--secondary').contains('Reset').click(); + + // Verify that the form was reset + cy.get('.flash_text_div > .alert').contains('All changes have been reset'); + cy.get('[name="name"]').should('have.value', 'TestClass'); + cy.get('[name="display_name"]').should('have.value', 'Test Class 0'); + cy.get('[name="description"]').should('have.value', 'This is a test class description'); + + // Click Cancel button + cy.get('[class="bx--btn bx--btn--secondary"]').contains('Cancel').click(); + + // Removes class + cy.get('[title="Configuration"]').click(); + cy.get('[title="Remove this Class"]').click(); + + // Verify that the class was removed + cy.get('div.alert.alert-success.alert-dismissable').should('exist').and('contain', 'Automate Class "TestClass": Delete successful'); + cy.get('#ns_details_div > .alert').contains('The selected Namespace is empty'); + }); + + it('Form validation works', () => { + // Try to create a Class with an invalid name + cy.get('[title="Datastore"]').click(); + cy.get('[title="Automate Domain: TestDomain"]').click(); // clicks on Domain + cy.get('[title="Automate Namespace: TestNameSpace"]').click(); // clicks on Namespace + + cy.get('[title="Configuration"]').click(); + cy.get('[title="Add a New Class"]').click(); + cy.get('[name="name"]').type('Test Class', {force: true}); + cy.get('[name="display_name"]').type('Test Class 0'); + cy.get('[name="description"').type('This is a test class description'); + + // Verify that error message appears + cy.get('#name-error-msg').contains('Name may contain only alphanumeric and _ . - characters'); + cy.get('.btnRight').should('be.disabled'); + + // Enter a valid name + cy.get('[name="name"]').clear({force: true}).type('TestClass'); + cy.get('.btnRight').click(); + + cy.get('#flash_msg_div .alert.alert-success').should('exist').and('be.visible').and('contain', 'Class "TestClass" was added'); + + // Navigate to the Properties tab of the class just created + cy.get('.clickable-row').contains('Test Class 0').click(); + cy.get('#props_tab > a').click(); + + // Verify that the class created correctly + cy.get(':nth-child(1) > .label_header').contains('Fully Qualified Name'); + cy.get(':nth-child(1) > .content_value').contains('/ TestDomain / TestNameSpace / TestClass'); + cy.get(':nth-child(2) > .label_header').contains('Name'); + cy.get(':nth-child(2) > .content_value').contains('TestClass'); + cy.get(':nth-child(3) > .label_header').contains('Display Name'); + cy.get(':nth-child(3) > .content_value').contains('Test Class 0'); + cy.get(':nth-child(4) > .label_header').contains('Description'); + cy.get(':nth-child(4) > .content_value').contains('This is a test class description'); + cy.get(':nth-child(5) > .label_header').contains('Instances'); + cy.get(':nth-child(5) > .content_value').contains('0'); + + cy.get('.list-group-item').then((listItems) => { + const nums = [...Array(listItems.length).keys()]; + nums.forEach((index) => { + if (listItems[index].innerText === 'TestNameSpace') { + cy.get(listItems[index]).click(); + } + }); + }); + + // Try to create a Class with the same name + cy.get('[title="Configuration"]').click(); + cy.get('[title="Add a New Class"]').click(); + cy.get('[name="name"]').type('TestClass', {force: true}); + cy.get('[name="display_name"]').type('Test Class 1'); + cy.get('[name="description"').type('This is a test class description'); + cy.get('.btnRight').click(); + + // Verify that error message appears + cy.get('.alert').contains('Name has already been taken'); + + // Enter a new name + cy.get('[name="name"]').clear({force: true}).type('NewTestClass', {force: true}); + cy.get('.btnRight').click(); + + // Check for the success message + cy.get('#flash_msg_div .alert.alert-success').should('exist').and('be.visible').and('contain', 'Class "NewTestClass" was added'); + + // Navigate to the Properties tab of the class just created + cy.get('.clickable-row').contains('Test Class 1').click(); + cy.get('#props_tab > a').click(); + + // Verify that the class created correctly + cy.get(':nth-child(1) > .label_header').contains('Fully Qualified Name'); + cy.get(':nth-child(1) > .content_value').contains('/ TestDomain / TestNameSpace / NewTestClass'); + cy.get(':nth-child(2) > .label_header').contains('Name'); + cy.get(':nth-child(2) > .content_value').contains('NewTestClass'); + cy.get(':nth-child(3) > .label_header').contains('Display Name'); + cy.get(':nth-child(3) > .content_value').contains('Test Class 1'); + cy.get(':nth-child(4) > .label_header').contains('Description'); + cy.get(':nth-child(4) > .content_value').contains('This is a test class description'); + cy.get(':nth-child(5) > .label_header').contains('Instances'); + cy.get(':nth-child(5) > .content_value').contains('0'); + + // Removes class + cy.get('[title="Configuration"]').click(); + cy.get('[title="Remove this Class"]').click(); + + // Verify that the class was removed + cy.get('div.alert.alert-success.alert-dismissable').should('exist').and('contain', 'Automate Class "NewTestClass": Delete successful'); + + // Navigate to the Properties tab of the first class just created + cy.get('.clickable-row').contains('Test Class 0').click(); + + // Removes class + cy.get('[title="Configuration"]').click(); + cy.get('[title="Remove this Class"]').click(); + + // Verify that the class was removed + cy.get('div.alert.alert-success.alert-dismissable').should('exist').and('contain', 'Automate Class "TestClass": Delete successful'); + cy.get('#ns_details_div > .alert').contains('The selected Namespace is empty'); + }); + + it('Creates and edits an automate class', () => { + // Creates a Class + cy.get('[title="Datastore"]').click({force: true}); + cy.get('[title="Automate Domain: TestDomain"]').click({force: true}); // clicks on Domain + cy.get('[title="Automate Namespace: TestNameSpace"]').click({force: true}); // clicks on Namespace + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Add a New Class"]').click({force: true}); + cy.get('[name="name"]').type('TestClass', {force: true}); + cy.get('[name="display_name"]').type('Test Class 0'); + cy.get('[name="description"').type('This is a test class description'); + cy.get('.bx--btn--primary').contains('Add').click(); + + // Check for the success message + cy.get('#flash_msg_div .alert.alert-success').should('exist').and('be.visible').and('contain', 'Class "TestClass" was added'); + + // Navigate to the Properties tab of the class just created + cy.get('.clickable-row').contains('Test Class 0').click(); + cy.get('#props_tab > a').click(); + + // Verify that the class created correctly + cy.get(':nth-child(1) > .label_header').contains('Fully Qualified Name'); + cy.get(':nth-child(1) > .content_value').contains('/ TestDomain / TestNameSpace / TestClass'); + cy.get(':nth-child(2) > .label_header').contains('Name'); + cy.get(':nth-child(2) > .content_value').contains('TestClass'); + cy.get(':nth-child(3) > .label_header').contains('Display Name'); + cy.get(':nth-child(3) > .content_value').contains('Test Class 0'); + cy.get(':nth-child(4) > .label_header').contains('Description'); + cy.get(':nth-child(4) > .content_value').contains('This is a test class description'); + cy.get(':nth-child(5) > .label_header').contains('Instances'); + cy.get(':nth-child(5) > .content_value').contains('0'); + + // Edits the class + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Edit this Class"]').click({force: true}); + cy.get('[name="name"]').type('Edit', {force: true}); + cy.get('[name="display_name"]').clear().type('Edited Test Class', {force: true}); + cy.get('[name="description"').clear().type('Edited test class description'); + cy.get('[class="btnRight bx--btn bx--btn--primary"]').contains('Save').click({force: true}); + + // Navigate to the Properties tab of the class just edited + cy.get('#props_tab a').click(); + + // Verify that the class edited correctly + cy.get(':nth-child(1) > .label_header').contains('Fully Qualified Name'); + cy.get(':nth-child(1) > .content_value').contains('/ TestDomain / TestNameSpace / TestClassEdit'); + cy.get(':nth-child(2) > .label_header').contains('Name'); + cy.get(':nth-child(2) > .content_value').contains('TestClassEdit'); + cy.get(':nth-child(3) > .label_header').contains('Display Name'); + cy.get(':nth-child(3) > .content_value').contains('Edited Test Class'); + cy.get(':nth-child(4) > .label_header').contains('Description'); + cy.get(':nth-child(4) > .content_value').contains('Edited test class description'); + cy.get(':nth-child(5) > .label_header').contains('Instances'); + cy.get(':nth-child(5) > .content_value').contains('0'); + + // Removes class + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Remove this Class"]').click({force: true}); + + // Verify that the class was removed + cy.get('div.alert.alert-success.alert-dismissable') + .should('exist') + .and('contain', 'Automate Class "TestClassEdit": Delete successful'); + cy.get('#ns_details_div > .alert').contains('The selected Namespace is empty'); + }); + }); + + after(() => { + // Remove the Domain after all the tests + cy.menu('Automation', 'Embedded Automate', 'Explorer'); + cy.get('[title="Datastore"]').click({force: true}); + cy.get('[title="Automate Domain: TestDomain"]').click({force: true}); + cy.get('[title="Configuration"]').click({force: true}); + cy.get('[title="Remove this Domain"]').click({force: true}); + + cy.get('.bx--data-table-content tbody tr').should('not.contain', 'Automate Domain: TestDomain'); + }); +}); diff --git a/spec/controllers/miq_ae_class_controller_spec.rb b/spec/controllers/miq_ae_class_controller_spec.rb index 954d325cbee..27964a84ac4 100644 --- a/spec/controllers/miq_ae_class_controller_spec.rb +++ b/spec/controllers/miq_ae_class_controller_spec.rb @@ -24,7 +24,7 @@ id = "aec-#{cls.id}" fq_name = cls.fqname controller.send(:set_right_cell_text, id, cls) - expect(assigns(:sb)[:namespace_path]).to eq(fq_name.gsub!(%r{\/}, " / ")) + expect(assigns(:sb)[:namespace_path]).to eq(fq_name) id = "root" fq_name = "" @@ -291,7 +291,7 @@ :active_tree => :ae_tree, :trees => {:ae_tree => {:active_node => node}}) controller.send(:get_node_info, node) - expect(assigns(:sb)[:namespace_path]).to eq(ns1.fqname.gsub!(%r{\/}, " / ")) + expect(assigns(:sb)[:namespace_path]).to eq(ns1.fqname) end end