-
Notifications
You must be signed in to change notification settings - Fork 366
Conversion of Service Dialogs Form from Angular to React #9592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
| @@ -0,0 +1,37 @@ | |||
| /* eslint-disable no-undef */ | |||
|
|
|||
| // Look for notification popups and disable them if present | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these for "real" notifications or for debug-type notifications? If the latter, then this shouldn't be needed because if you run the webpack-dev-server and the rails server with CYPRESS=true then those debug-type notifications won't be there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I went the same way, creating a command to get rid of these notifications before I knew CYPRESS variable to do CYPRESS=true bin/webpack for compiling assets for development
| /* eslint-disable no-undef */ | ||
|
|
||
| // Drag and drop a component to a section | ||
| Cypress.Commands.add('dragAndDropComponent', (componentName, targetSectionIndex = 0) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because there are commands being defined globally, but are specific to service dialogs, I think the names should be prefixed with something like serviceDialog to avoid any name collisions and to clarify their purpose.
| }); | ||
|
|
||
| // Login and navigate to add a new service dialog | ||
| Cypress.Commands.add('navigateToAddDialog', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, maybe not this one because it also includes login and navigation
|
I know this is still WIP, but can @GilbertCherrie , @asirvadAbrahamVarghese please review to get a first pass on it? |
| import PropTypes from 'prop-types'; | ||
| import { InlineNotification } from 'carbon-components-react'; | ||
|
|
||
| const InlineFlashMessage = ({ message, setMessage }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- if it’s just to pass through any actions required for the close button click maybe we could rename the prop to something like
onCloseClick(I’m clueless when it comes to naming, so it’s your call) - Should we consider changing
messagetomessageInfo, given that its an object? - again thats your call
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if it’s just to pass through any actions required for the close button click maybe we could rename the prop to something like onCloseClick
This component pulls InlineNotification component from carbon-components-react and is used to display contextual messages inline with other contents in the UI. onCloseButtonClick is actually a property of the InlineNotification component - It is an event handler that is only called when the user clicks the close button(if it is visible).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant the setMessage prop from our component that we’re using as the handler for onCloseButtonClick in InlineNotification, I feel like onCloseButtonClick ={onCloseClick} (or anything similar) seems better in this context
| title={message.title || ''} | ||
| subtitle={message.subtitle || ''} | ||
| lowContrast | ||
| hideCloseButton={!setMessage} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to control hideCloseButton through a separate boolean prop and set a default for it 🤔
| lowContrast | ||
| hideCloseButton={!setMessage} | ||
| onCloseButtonClick={ | ||
| typeof setMessage === 'function' ? () => setMessage(null) : undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking if we should directly do:
onCloseButtonClick={setMessage}
So that react will pass down the event automatically when setMessage handler is called similar to how we’d pass the event manually like:
onCloseButtonClick={(event)=>setMessage(event)}
So if in case we want to access any event methods (preventDefault, stopPropagation) from this component sometime later we can do:
<InlineFlashMessage setMessage={(event)=>{ event.preventDefault(); }} .... />
| }; | ||
|
|
||
| InlineFlashMessage.defaultProps = { | ||
| message: null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we are doing if (!message) return null; at the top of the component, we can safely skip setting message as null from default props unless its marked required by PropTypes.shape({}).isRequired - the value received will be undefined and would still match our if(!message) condition
|
|
||
| InlineFlashMessage.defaultProps = { | ||
| message: null, | ||
| setMessage: undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can set a no-op function as default instead of undefined:
setMessage: () => {},
If thats throwing any eslint error(empty blocks), we can do:
setMessage: () => {
// default callback
},
so that it prevents errors like “Cannot call setMessage of undefined”.
this also means we don’t have to explicitly verify typeof setMessage === 'function' before passing existing function reference directly: onCloseButtonClick={setMessage}
or even if we are creating a new arrow function on every render that calls onCloseButtonClick:
onCloseButtonClick={() => {
// any other action
setMessage();
}}
| } from 'carbon-components-react'; | ||
| import { getCurrentDate, getCurrentTimeAndPeriod } from '../service-dialog-form/helper'; | ||
|
|
||
| const CustomDateTimePicker = (field) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me here if I am wrong
We will be using the component like:
<CustomDateTimePicker
label="Custom Date Time Picker"
initialData={{ date: '11/06/2025', time: '23:11', period: 'PM' }}
onChange={(data) => {
// any actions with on change data
}}
/>
So I was thinking - just like we did with InlineFlashMessage, inline destructuring of component props is generally cleaner instead of accessing them via field object:
const CustomDateTimePicker = ({ label, onChange, initialData }) => {...
and then add props validations and default props for them:
CustomDateTimePicker.propTypes = {
initialData: PropTypes.shape({
date: PropTypes.string,
time: PropTypes.string,
period: PropTypes.string,
}),
onChange: PropTypes.func,
label: PropTypes.string,
};
CustomDateTimePicker.defaultProps = {
initialData: { date: '', time: '', period: '' },
onChange: () => {},
label: '',
};
Note: date, time and period having an empty string as default value ({ date: '', time: '', period: '' }) shouldn't be a problem as they are falsy in JS, fallback methods like getCurrentDate and getCurrentTimeAndPeriod will still be triggered to set states initial values
| const CustomDateTimePicker = (field) => { | ||
| const { initialData, onChange } = field; | ||
|
|
||
| const [date, setDate] = useState(initialData.date || getCurrentDate); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it would be better to call the function immediately (getCurrentDate) instead of passing the reference for React to call it automatically to get the initial state value when initialData.date is not available:
const [date, setDate] = useState(() => initialData.date || getCurrentDate());
also include the lazy initialization () =>🔝, like we did with time and period states initializations, so that the arrow function is only executed during the initial render, on subsequent re-renders, react knows it already has a state value and doesn't need to call the function again. The performance difference would be negligible here I believe, but it's better to use the arrow function for any initialization that creates objects, makes calculations or anything that could potentially be expensive.
| const { initialData, onChange } = field; | ||
|
|
||
| const [date, setDate] = useState(initialData.date || getCurrentDate); | ||
| const [time, setTime] = useState(() => initialData.time || getCurrentTimeAndPeriod().time); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On initial render, time would look like this which is not in our expected "hh:mm" format, it should have been "09:54", But the invalid time error: "Enter a valid 12-hour time" is not displayed, probably thats because isValid state is initially true

Right after time field is edited the error is displayed since our state isValid state is now updated:

No issues with this implementation, this should work well once we apply zero-padding to the hour value in getCurrentTimeAndPeriod(similar to what we did with minutes).
It should be padded after converting to 12-hour format, otherwise, the modulo will reduce it to a single digit again.
hours = ${hours % 12 || 12}.padStart(2, '0');
Which should work well:

| }; | ||
|
|
||
| const handleDateChange = (newDate) => { | ||
| if (newDate.length > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (newDate.length) should be fine here since 0 is also falsy in JS and any number greater than 0 will be truthy
| const newTime = event.target.value; | ||
| setTime(newTime); | ||
| validateTime(newTime); | ||
| if (isValid) onChange({ value: combinedDateTime(), initialData }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried "11:5" in the time field, which doesn't match our pattern, so it's treated as an invalid time, the regex correctly returns false, which we also assign to the isValid state:

Given the condition(if (isValid) onChange({ ... });), we don't expect the onChange prop to be triggered in this case but it still gets called since isValid is still true(initial state value is true):

This is because state updates are asynchronous & batched and won't be reflecting immediately, when setIsValid is called react schedules the state update and updated value will be available only in the next render(only after the current event scope) not immediately after the call. So within the same event, we will still be accessing the previous state(which is true).
Returning the regex result from validateTime and using it for both setting state and controlling onChange should work fine:
const validateTime = (value) => {
const timeRegex = /^(0[1-9]|1[0-2]):[0-5][0-9]$/; // Matches 12-hour format hh:mm
return timeRegex.test(value);
};
const handleTimeChange = (event) => {
const newTime = event.target.value;
setTime(newTime);
const isValidTime = validateTime(newTime);
setIsValid(isValidTime);
if (isValidTime) onChange({ value: combinedDateTime(), initialData });
};
| const newTime = event.target.value; | ||
| setTime(newTime); | ||
| validateTime(newTime); | ||
| if (isValid) onChange({ value: combinedDateTime(), initialData }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The combinedDateTime function is also impacted the same way, we are updating the time state from here & then trying to access the state value from the same event scope, we will be having the previous time value due to react's batched updates.
Time field value is updated to "12:53" but time state will still provide its previous value "12:5" inside the same method scope:

Instead of relying on stale state we could update combinedDateTime to use the same input we're using for the state (in a way that it uses parameter value if available or the state value, thus other state values(date & period) are set):
const combinedDateTime = ({ dateValue, timeValue, periodValue } = {}) =>
`${dateValue || date} ${timeValue || time} ${periodValue || period}`;
🔝 we can also avoid the intermediate variable(const dateTime) by returning the string immediately
Now from handleTimeChange we can pass the value for time like:
if (isValidTime) {
onChange({
value: combinedDateTime({ timeValue: newTime }),
initialData,
});
}
So that correct updated time is returned from combinedDateTime

| year: 'numeric', | ||
| }).format(newDate[0]); | ||
| setDate(formattedDate); | ||
| onChange({ value: combinedDateTime(), initialData }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like discussed below, due to batched state updates we might want to update combinedDateTime to receive the updated date string:
const combinedDateTime = ({ dateValue, timeValue, periodValue } = {}) =>
`${dateValue || date} ${timeValue || time} ${periodValue || period}`;
and then from handleDateChange pass the date string value:
const handleDateChange = (newDate) => {
if (newDate.length) {
const formattedDate = new Intl.DateTimeFormat('en-US', {
.....
onChange({
value: combinedDateTime({ dateValue: formattedDate }),
initialData,
});
}
};
|
|
||
| const handlePeriodChange = (event) => { | ||
| setPeriod(event.target.value); | ||
| onChange({ value: combinedDateTime(), initialData }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, due to batched state updates we might want to update combinedDateTime to receive the updated period string:
const combinedDateTime = ({ dateValue, timeValue, periodValue } = {}) =>
`${dateValue || date} ${timeValue || time} ${periodValue || period}`;
and then from handlePeriodChange pass the updated period value:
const handlePeriodChange = (event) => {
const newPeriod = event.target.value;
setPeriod(newPeriod);
onChange({
value: combinedDateTime({ periodValue: newPeriod }),
initialData,
});
};
|
|
||
| return ( | ||
| <div> | ||
| <FormLabel>{field.label}</FormLabel> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, {field.label} will always be present. It comes from the dynamic-field-configuration file
defaultDateTimePickerValue: { **label: __('Default value')**, name: 'value', field: 'date-time-picker' },
| SelectItem, | ||
| FormLabel, | ||
| } from 'carbon-components-react'; | ||
| import { getCurrentDate, getCurrentTimeAndPeriod } from '../service-dialog-form/helper'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dealing with dates can be bug-prone. we should be fine for now with simple date logics but as logic grows, pulling in a utility like date-fns would help.
78be4a5 to
ad336c6
Compare
| schema={createSchema(usedTabNames, tabInfo.name)} | ||
| initialValues={{ | ||
| tab_name: tabInfo.name, | ||
| tab_description: tabInfo.description, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| }; | ||
|
|
||
| // Custom form template | ||
| const FormTemplate = ({ formFields }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better if we can move this FormTemplate component outside of EditFieldModal component
It gets recreated on every render of EditFieldModal
| @@ -0,0 +1,730 @@ | |||
| /* eslint-disable radix */ | |||
| import React, { useState, useRef, useEffect, useCallback } from 'react'; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like useCallback is not used
| const dragEnterItem = useRef(null); /** Stores the information of component where the dragged item is being hovered before release. */ | ||
| const draggedItem = useRef(null); /** Stores the information of component being dragged. */ | ||
| const hoverItem = useRef(null); /** Stores the tab and section position during the drop event. */ | ||
| const nextSectionId = useRef(1); /** Counter for generating unique section IDs */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it looks neat to use useRef for this, since the counter doesn’t need to cause unnecessary re-renders and the value sticks around
I’m just not sure if there are any downsides compared to using the state managing hooks like useState/useReducer
| } | ||
| } | ||
| }) | ||
| .catch((error) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to display the API error on the UI so it’s clear what failed? If that’s not required, we could just add an underscore prefix _error
| } | ||
| }, [dialogData, dialogAction]); | ||
|
|
||
| const [isSubmitButtonEnabled, setIsSubmitButtonEnabled] = useState(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For complex components, it’s better if we keep all the state up at the top of the component
| evaluateSubmitButton(); | ||
| }, [data]); | ||
|
|
||
| const onTabAction = (type, event) => tabAction({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like onTabAction is not used
| const [selTab, setSelTab] = useState(null); | ||
|
|
||
| const [showSectionEditModal, setSectionEditModal] = useState(false); | ||
| const [selSection, setSelSection] = useState(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like mentioned above, better if we can move these states to the top
|
|
||
|
|
||
| const onModalHide = () => setTabEditModal(false); | ||
| const onModalShow = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like onModalHide & onModalShow are not used
|
|
||
| /** Function to render the Tab's name. */ | ||
| const renderTabName = (tab) => ( | ||
| <div className="dynamic-tab-name"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking maybe we could prefix these classnames with the component name, like service-dialog-dynamic-tab-name
I’m just being cautious because while doing the C11 migration, I’ve noticed styles keep getting overridden here and there because of duplicate class names
| {renderTabs()} | ||
| </Tabs> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if these renderTabName, renderSections, renderAddSectionButton , renderTabs, renderTabContents should be moved outside to avoid re-creation on every parent component render
| description: data.description, | ||
| dialog_tabs: data.formFields | ||
| .filter((tab) => tab.tabId !== 'new') // Filter out the "Create new tab" option | ||
| .map((tab, index) => ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could turn these filter and map iterations into a single reduce to avoid looping over the array multiple times?
| } | ||
| </div> | ||
| {/* Form submit/cancel buttons */} | ||
| <div className="custom-button-wrapper"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thought as I mentioned here about class names
| return result; | ||
| }; | ||
|
|
||
| const getFieldsInfo = (fields) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is no other logic to add, we can use parseFieldsInfo directly when required instead of a wrapper getFieldsInfo
|
|
||
| const getTabsInfo = (tabs) => | ||
| tabs | ||
| .map((tab) => ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like mentioned above we can use reduce instead of map + filter I believe
| }); | ||
|
|
||
| export const formattedCatalogPayload = (data) => { | ||
| const payload = payloadForSave(data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can do this directly instead of using payloadForSave
export const formattedCatalogPayload = (data) => {
const payload = { action: 'create', resource: getDialogInfo(data) };
return payload;
};
| const trimmed = value.trim().toLowerCase(); | ||
|
|
||
| const isDuplicate = usedNames | ||
| .filter((name) => name.toLowerCase() !== currentName.toLowerCase()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can do this using some instead of filter, map and includes
const isDuplicate = usedNames.some((name) =>
name.toLowerCase() !== currentName.toLowerCase() &&
name.toLowerCase() === trimmed
);
| ManageIQ.component.addReact('MiqCustomTab', MiqCustomTab); | ||
| ManageIQ.component.addReact('MiqDataTable', MiqDataTable); | ||
| ManageIQ.component.addReact('MiqMarkdown', MiqMarkdown); | ||
| ManageIQ.component.addReact('ServiceDialogForm', ServiceDialogForm); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to maintain alphabetical order here?
| margin-right: 5%; | ||
| } | ||
| } | ||
| // CSS to adjust the UI for field array items in service dialog form |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove these if they won’t be required later on...
| }; | ||
|
|
||
| // Get the current time and period | ||
| export const getCurrentTimeAndPeriod = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out we have moment lib, so may be we could use that to format dates & times:
const now = moment();
return {
time: now.format("hh:mm"),
period: now.format("A")
};
Moment should handle the 12-hour conversion and zero-padding automatically I guess
|
|
||
| // Look for a server error pop up and close if visible | ||
| Cypress.Commands.add('closeErrorPopupIfVisible', () => { | ||
| cy.get('body').then(($body) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const renderTabs = () => data.formFields.map((tab, tabPosition) => ( | ||
| <Tab | ||
| key={`tab${tabPosition.toString()}`} | ||
| draggable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
||
| // Enable Required | ||
| cy.get('.edit-field-modal input[name="required"]') | ||
| .check({ force: true }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m guessing the force: true is needed because the input is small and Cypress thinks it’s hidden. Maybe we can target the label with the for attribute instead to interact with the input, like we did for the Default Value checkbox:
cy.get('.edit-field-modal label[for="checked"]').click();
|
|
||
| it('should disable save button when label or name is not entered', () => { | ||
| const getSubmitButton = () => { | ||
| return cy.get('.edit-field-modal button[type="submit"]') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to make the form element selectors more generic, you can use the selectors from the support commands for buttons, selects, labels, legends, and inputs. But if you’ve got two forms showing at the same time, things could get messy, for example, if you try to target the Cancel button in the modal form while there’s also a Cancel button in the main form, both will match and you’ll end up indexing them, which isn’t reliable I think
| Cypress.Commands.add('navigateToAddDialog', () => { | ||
| cy.login(); | ||
| cy.intercept('POST', '/ops/accordion_select?id=dialogs_accord'); // May not be needed; added to check the server error shown sometimes | ||
| cy.intercept('POST', '/ops/accordion_select?id=rbac_accord').as('accordion'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accordion-select and Tree-select APIs are taken care of at the support command level (accordion & selectAccordionItem), so tests don’t need to handle them anymore unless we need to register a new intercept handler
| .click(); | ||
|
|
||
| // Step 10: Save the dialog | ||
| cy.get('.service-dialog-main-wrapper .custom-button-wrapper button').contains('Submit').click(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to target buttons using their text, it’s probably better to just use contains:
cy.contains('.service-dialog-main-wrapper .custom-button-wrapper button', 'Submit').click();
|
@jrafanie If you get a chance, can you review the Cypress here? |
| cy.dragAndDropComponent('Check Box'); | ||
| }); | ||
|
|
||
| it('should verify default properties of the checkbox', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just searching for "should verify default properties", I can see each type of checkbox, datepicker, dropdown, etc. has a lot of similar assertions. Maybe they can be refactored to have shared helper functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refactoring doesn't need to happen right now, but we're more likely to do it now vs. later.
|
|
||
| // Enable dynamic option | ||
| cy.get('.edit-field-modal input[name="dynamic"]') | ||
| .check({ force: true }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
force: true statements should have an explanation why they're needed... if it's being covered by another element, maybe there's a better way to do it by waiting for the object to be fully visible and accessible.
| cy.openFieldEditModal(0, 0, 0); | ||
|
|
||
| // Initial state - button should be disabled | ||
| verifyButtonState(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of the comment, maybe the function call should look like this:
verifyButtonEnabled(false);Currently, it looks like verifyButtonState(true); expect disabled... and verifyButtonState(false); means enabled which is negative logic. I think this might be easier to read:
verifyButtonEnabled(false); // disabled
verifyButtonEnabled(true); // enabledOr even:
verifyButtonState('enabled');
verifyButtonState('disabled');(but then you need to check for strings... maybe my first suggestion is clearer. What do you think?






@miq-bot add-label enhancement
@miq-bot add-label wip