Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cypress/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ ManageIQ implements the following cypress extensions:
* `cy.getFormSelectFieldById({ selectId })` - retrieves a form select field by its ID. `selectId` is the ID of the select field. e.g. `cy.getFormSelectFieldById({selectId: 'select-scan-limit'});`
* `cy.getFormTextareaById({ textareaId })` - retrieves a form textarea field by its ID. `textareaId` is the ID of the textarea field. e.g. `cy.getFormTextareaById({textareaId: 'default.auth_key'});`

##### form_elements_validation_commands

* `cy.validateFormLabels(labelConfigs)` - validates form field labels based on provided configurations. `labelConfigs` is an array of label configuration objects with properties: `forValue` (required) - the 'for' attribute value of the label, `expectedText` (optional) - the expected text content of the label. e.g. `cy.validateFormLabels([{ forValue: 'name', expectedText: 'Name' }, { forValue: 'email', expectedText: 'Email Address' }]);` or using constants: `cy.validateFormLabels([{ [LABEL_CONFIG_KEYS.FOR_VALUE]: 'name', [LABEL_CONFIG_KEYS.EXPECTED_TEXT]: 'Name' }]);`
* `cy.validateFormFields(fieldConfigs)` - validates form input fields based on provided configurations. `fieldConfigs` is an array of field configuration objects with properties: `id` (required) - the ID of the form field, `fieldType` (optional, default: 'input') - the type of field ('input', 'select', 'textarea'), `inputFieldType` (optional, default: 'text') - the type of input field ('text', 'password', 'number'), `shouldBeDisabled` (optional, default: false) - whether the field should be disabled, `expectedValue` (optional) - the expected value of the field. e.g. `cy.validateFormFields([{ id: 'name', shouldBeDisabled: true }, { id: 'role', fieldType: 'select', expectedValue: 'admin' }]);` or using constants: `cy.validateFormFields([{ [FIELD_CONFIG_KEYS.ID]: 'email', [FIELD_CONFIG_KEYS.INPUT_FIELD_TYPE]: 'email' }, { [FIELD_CONFIG_KEYS.ID]: 'name', [FIELD_CONFIG_KEYS.SHOULD_BE_DISABLED]: true }]);`
* `cy.validateFormFooterButtons(buttonConfigs)` - validates form buttons based on provided configurations. `buttonConfigs` is an array of button configuration objects with properties: `buttonText` (required) - the text of the button, `buttonType` (optional, default: 'button') - the type of button (e.g., 'submit', 'reset'), `shouldBeDisabled` (optional, default: false) - whether the button should be disabled. e.g. `cy.validateFormFooterButtons([{ buttonText: 'Cancel' }, { buttonText: 'Submit', buttonType: 'submit', shouldBeDisabled: true }]);` or using constants: `cy.validateFormFooterButtons([{ [BUTTON_CONFIG_KEYS.TEXT]: 'Cancel' }]);`

#### Assertions

* `cy.expect_explorer_title(title)` - check that the title on an explorer screen matches the provided title. `title`: String for the title.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/* eslint-disable no-undef */
// TODO: Use aliased import(@cypress-dir) once #9631 is merged
import { flashClassMap } from '../../../../support/assertions/assertion_constants';
import {
LABEL_CONFIG_KEYS,
FIELD_CONFIG_KEYS,
BUTTON_CONFIG_KEYS,
} from '../../../../support/commands/constants/command_constants';

// Menu options
const SETTINGS_MENU_OPTION = 'Settings';
Expand Down Expand Up @@ -74,34 +80,39 @@ describe('Automate C & U Gap Collection form operations: Settings > Application
cy.contains('#main-content .bx--form h3', FORM_SUBHEADER_SNIPPET).should(
'be.visible'
);
// Assert timezone label & field is visible and enabled
cy.getFormLabelByForAttribute({ forValue: 'timezone' })
.should('be.visible')
.and('contain.text', TIMEZONE_FIELD_LABEL);
cy.getFormInputFieldByIdAndType({ inputId: 'timezone' })
.should('be.visible')
.and('be.enabled');
// Assert start date label & field is visible and enabled
cy.getFormLabelByForAttribute({ forValue: 'startDate' })
.should('be.visible')
.and('contain.text', START_DATE_FIELD_LABEL);
cy.getFormInputFieldByIdAndType({ inputId: 'startDate' })
.should('be.visible')
.and('be.enabled');
// Assert end date label & field is visible and enabled
cy.getFormLabelByForAttribute({ forValue: 'endDate' })
.should('be.visible')
.and('contain.text', END_DATE_FIELD_LABEL);
cy.getFormInputFieldByIdAndType({ inputId: 'endDate' })
.should('be.visible')
.and('be.enabled');
// Assert save button is visible and disabled
cy.getFormFooterButtonByTypeWithText({
buttonText: 'Save',
buttonType: 'submit',
})
.should('be.visible')
.and('be.disabled');

// Validate form labels
cy.validateFormLabels([
{
[LABEL_CONFIG_KEYS.FOR_VALUE]: 'timezone',
[LABEL_CONFIG_KEYS.EXPECTED_TEXT]: TIMEZONE_FIELD_LABEL,
},
{
[LABEL_CONFIG_KEYS.FOR_VALUE]: 'startDate',
[LABEL_CONFIG_KEYS.EXPECTED_TEXT]: START_DATE_FIELD_LABEL,
},
{
[LABEL_CONFIG_KEYS.FOR_VALUE]: 'endDate',
[LABEL_CONFIG_KEYS.EXPECTED_TEXT]: END_DATE_FIELD_LABEL,
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to be honest, this is hard for me to read. I would expect the validations to be an array of key, value pairs where the key should be the identifier/locator for the lookup, and the value is the expected value. Maybe we should in-line the constant values here so it's clear what the test is doing. In other words, what we're hiding in constants here is directly relevant to the test I think it's fine to have some duplication. There's no need to abstract them away or add a layer of indirection.

What do they all have in common? A CSS selector or similar and a contain.text assertion. Maybe an array of key/value pairs where the key is the selector and the value is the text to be asserted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key/value pairs where the key is the selector and the value is the text to be asserted?

If we just turn this into a simple object with selectors/locators(id, for...) as keys and expected text as values, it’d look something like this:

Cypress.Commands.add('validateFormLabels', (labelConfigs) => {
  if (!labelConfigs || typeof labelConfigs !== 'object' || Array.isArray(labelConfigs)) {
    cy.logAndThrowError('labelConfigs must be a non-array object');
  }
  const entries = Object.entries(labelConfigs);
  if (!entries.length) {
    cy.logAndThrowError('labelConfigs object cannot be empty');
  }
  entries.forEach(([forValue, expectedText]) => {
    if (!forValue) {
      cy.logAndThrowError('Label selector (for attribute) cannot be empty');
    }
    const labelCheck = cy
      .getFormLabelByForAttribute({ forValue })
      .should('be.visible');
    if (expectedText) {
      labelCheck.and('contain.text', expectedText);
    }
  });
});

The issue here is that when we need to support additional options(like in the other 2 commands validateFormFields & validateFormFooterButtons), the only way to do so is by adding a delimiter (like - or |) in the value string(which currently only contains the expected text) and then do the other options logic in the command, which feels quite messy to me.
For example, if we want to scroll the label element into view before making assertions, we’d have to write something like:
cy.validateFormLabels({ timezone: Timezone | scrollIntoView' });

But with our current structure we can just add a new key:

cy.validateFormLabels([
      {
        scrollIntoView: true,
        forValue: 'timezone',
        expectedVale: 'Timezone',
      },
    ]);

what we're hiding in constants here is directly relevant to the test I think it's fine to have some duplication

If we're referring to the config-keys object(LABEL_CONFIG_KEYS), its main purpose is to prevent the use of unknown or misspelled keys. For example, if I am using "expectedVale" instead of "expectedValue":

    cy.validateFormLabels([
      {
        forValue: 'timezone',
        expectedVale: 'Timezone',
      },
      {
        forValue: 'startDate',
        expectedVale: 'Start Date',
      },
    ]);
image 🔝 here the "Timezone" label value is not being asserted in the same way as "Start Date", which was our original intention.

The test will still run without throwing any errors, unless we explicitly validate that every key passed to the command exists in the config-keys object(LABEL_CONFIG_KEYS).

Let me know if anything stands out or needs tweaking...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, good point.. I'm not sure what's preferable... the current interface is hard to reason about for me right now. I feel like the tester should know what fields it wants to validate... if they typo the field, we should assert it's wrong so they can fix it. I "think" class/ids in the dom are less likely to change than message catalogs / strings so I'm fine with the repetitive nature of putting the field id/class/selector in the key. I'm not sure.

The test will still run without throwing any errors, unless we explicitly validate that every key passed to the command exists in the config-keys object(LABEL_CONFIG_KEYS).

I think if you assert a field has a value and the field doesn't exist, we should raise an error.

Copy link
Contributor Author

@asirvadAbrahamVarghese asirvadAbrahamVarghese Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you assert a field has a value and the field doesn't exist, we should raise an error.

Cypress will fail if the field isn’t found (like if the ID is incorrect):
image

I was referring to configuration keys (like forValue, expectedValue). Earlier, typos in these keys wouldn't cause any errors, the test would still execute. I've now added validation to catch unrecognised keys upfront.
image

image image

]);
// Validate form fields
cy.validateFormFields([
{
[FIELD_CONFIG_KEYS.ID]: 'timezone',
[FIELD_CONFIG_KEYS.EXPECTED_VALUE]: '(GMT+00:00) UTC',
},
{ [FIELD_CONFIG_KEYS.ID]: 'startDate' },
{ [FIELD_CONFIG_KEYS.ID]: 'endDate' },
]);
// Validate form footer buttons
cy.validateFormFooterButtons([
{
[BUTTON_CONFIG_KEYS.BUTTON_TEXT]: 'Save',
[BUTTON_CONFIG_KEYS.BUTTON_TYPE]: 'submit',
[BUTTON_CONFIG_KEYS.SHOULD_BE_DISABLED]: true,
},
]);
});

it('Should fail if start date is greater than end date', () => {
Expand Down
35 changes: 35 additions & 0 deletions cypress/support/commands/constants/command_constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Form elements validation constants
* ==========================================================
*/

// Form label configuration keys
export const LABEL_CONFIG_KEYS = {
FOR_VALUE: 'forValue',
EXPECTED_TEXT: 'expectedText',
};

// Form user input field configuration keys
export const FIELD_CONFIG_KEYS = {
ID: 'id',
FIELD_TYPE: 'fieldType',
INPUT_FIELD_TYPE: 'inputFieldType',
SHOULD_BE_DISABLED: 'shouldBeDisabled',
EXPECTED_VALUE: 'expectedValue',
};

// Form field types
export const FIELD_TYPES = {
INPUT: 'input',
SELECT: 'select',
TEXTAREA: 'textarea',
};

// Form button configuration keys
export const BUTTON_CONFIG_KEYS = {
BUTTON_TEXT: 'buttonText',
BUTTON_TYPE: 'buttonType',
SHOULD_BE_DISABLED: 'shouldBeDisabled',
};

/* ========================================================== */
254 changes: 254 additions & 0 deletions cypress/support/commands/form_elements_validation_commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// TODO: Use aliased import(@cypress-dir) once #9631 is merged
import {
LABEL_CONFIG_KEYS,
FIELD_CONFIG_KEYS,
FIELD_TYPES,
BUTTON_CONFIG_KEYS,
} from './constants/command_constants.js';

/**
* Helper function to validate that config objects only contain valid keys
*
* @param {Object} config - The configuration object to validate
* @param {Object} validKeysObject - The object containing valid keys (e.g., LABEL_CONFIG_KEYS)
* @param {string} configType - The type of configuration being validated (for error messages)
*/
const validateConfigKeys = (config, validKeysObject, configType) => {
const validKeys = Object.values(validKeysObject);

Object.keys(config).forEach((key) => {
if (!validKeys.includes(key)) {
cy.logAndThrowError(
`Unknown key "${key}" in ${configType} config. Valid keys are: ${validKeys.join(
', '
)}`
);
}
});
};

/**
* Validates form field labels based on provided configurations
*
* @param {Array} labelConfigs - Array of label configuration objects
* @param {string} labelConfigs[].forValue - The 'for' attribute value of the label
* @param {string} [labelConfigs[].expectedText] - The expected text content of the label
*
* Example:
* cy.validateFormLabels([
* { [LABEL_CONFIG_KEYS.FOR_VALUE]: 'name', [LABEL_CONFIG_KEYS.EXPECTED_TEXT]: 'Name' },
* { [LABEL_CONFIG_KEYS.FOR_VALUE]: 'email', [LABEL_CONFIG_KEYS.EXPECTED_TEXT]: 'Email Address' }
* ]);
*
* Or using regular object keys:
* cy.validateFormLabels([
* { forValue: 'name', expectedText: 'Name' },
* { forValue: 'email', expectedText: 'Email Address' }
* ]);
*
* Both approaches work but using config-keys object(LABEL_CONFIG_KEYS) is recommended to avoid typos and unknown keys
*/
Cypress.Commands.add('validateFormLabels', (labelConfigs) => {
if (!Array.isArray(labelConfigs)) {
cy.logAndThrowError('labelConfigs must be an array');
}

if (!labelConfigs.length) {
cy.logAndThrowError('labelConfigs array cannot be empty');
}

labelConfigs.forEach((config) => {
validateConfigKeys(config, LABEL_CONFIG_KEYS, 'label');

const forValue = config[LABEL_CONFIG_KEYS.FOR_VALUE];
const expectedText = config[LABEL_CONFIG_KEYS.EXPECTED_TEXT];

if (!forValue) {
cy.logAndThrowError(
`${LABEL_CONFIG_KEYS.FOR_VALUE} is required for each label config`
);
}

const labelCheck = cy
.getFormLabelByForAttribute({ forValue })
.should('be.visible');

if (expectedText) {
labelCheck.and('contain.text', expectedText);
}
});
});

/**
* Validates form input fields based on provided configurations
*
* @param {Array} fieldConfigs - Array of field configuration objects
* @param {string} fieldConfigs[].id - The ID of the form field
* @param {string} [fieldConfigs[].fieldType='input'] - The type of field ('input', 'select', 'textarea')
* @param {string} [fieldConfigs[].inputFieldType='text'] - The type of input field ('text', 'password', 'number')
* @param {boolean} [fieldConfigs[].shouldBeDisabled=false] - Whether the field should be disabled
* @param {string} [fieldConfigs[].expectedValue] - The expected value of the field
*
* Example:
* cy.validateFormFields([
* { [FIELD_CONFIG_KEYS.ID]: 'name', [FIELD_CONFIG_KEYS.SHOULD_BE_DISABLED]: true },
* { [FIELD_CONFIG_KEYS.ID]: 'email', [FIELD_CONFIG_KEYS.INPUT_FIELD_TYPE]: 'email' },
* {
* [FIELD_CONFIG_KEYS.ID]: 'role',
* [FIELD_CONFIG_KEYS.FIELD_TYPE]: FIELD_TYPES.SELECT,
* [FIELD_CONFIG_KEYS.EXPECTED_VALUE]: 'admin'
* }
* ]);
*
* Or using regular object keys:
* cy.validateFormFields([
* { id: 'name', shouldBeDisabled: true },
* { id: 'email' },
* { id: 'role', fieldType: 'select', expectedValue: 'admin' }
* ]);
*
* Both approaches work but using config-keys object(FIELD_CONFIG_KEYS) is recommended to avoid typos and unknown keys
*/
Cypress.Commands.add('validateFormFields', (fieldConfigs) => {
if (!Array.isArray(fieldConfigs)) {
cy.logAndThrowError('fieldConfigs must be an array');
}

if (!fieldConfigs.length) {
cy.logAndThrowError('fieldConfigs array cannot be empty');
}

fieldConfigs.forEach((config) => {
validateConfigKeys(config, FIELD_CONFIG_KEYS, 'field');

const id = config[FIELD_CONFIG_KEYS.ID];
const fieldType = config[FIELD_CONFIG_KEYS.FIELD_TYPE] || FIELD_TYPES.INPUT;
const inputFieldType = config[FIELD_CONFIG_KEYS.INPUT_FIELD_TYPE] || 'text';
const shouldBeDisabled =
config[FIELD_CONFIG_KEYS.SHOULD_BE_DISABLED] || false;
const expectedValue = config[FIELD_CONFIG_KEYS.EXPECTED_VALUE];

if (!id) {
cy.logAndThrowError(
`${FIELD_CONFIG_KEYS.ID} is required for each field config`
);
}

// Check field based on type
switch (fieldType) {
case FIELD_TYPES.INPUT:
cy.getFormInputFieldByIdAndType({
inputId: id,
inputType: inputFieldType,
})
.should('be.visible')
.then((field) => {
if (shouldBeDisabled) {
expect(field).to.be.disabled;
} else {
expect(field).to.not.be.disabled;
}

if (expectedValue) {
cy.wrap(field).should('have.value', expectedValue);
}
});
break;
case FIELD_TYPES.SELECT:
cy.getFormSelectFieldById({ selectId: id })
.should('be.visible')
.then((field) => {
if (shouldBeDisabled) {
expect(field).to.be.disabled;
} else {
expect(field).to.not.be.disabled;
}

if (expectedValue) {
cy.wrap(field).should('have.value', expectedValue);
}
});
break;
case FIELD_TYPES.TEXTAREA:
cy.getFormTextareaById({ textareaId: id })
.should('be.visible')
.then((field) => {
if (shouldBeDisabled) {
expect(field).to.be.disabled;
} else {
expect(field).to.not.be.disabled;
}

if (expectedValue) {
cy.wrap(field).should('have.value', expectedValue);
}
});
break;

default:
cy.logAndThrowError(`Unsupported field type: ${fieldType}`);
}
});
});

/**
* Validates form buttons based on provided configurations
*
* @param {Array} buttonConfigs - Array of button configuration objects
* @param {string} buttonConfigs[].buttonText - The text of the button
* @param {string} [buttonConfigs[].buttonType='button'] - The type of button (e.g., 'submit', 'reset')
* @param {boolean} [buttonConfigs[].shouldBeDisabled=false] - Whether the button should be disabled
*
* Example:
* cy.validateFormFooterButtons([
* { [BUTTON_CONFIG_KEYS.BUTTON_TEXT]: 'Cancel' },
* { [BUTTON_CONFIG_KEYS.BUTTON_TEXT]: 'Reset', [BUTTON_CONFIG_KEYS.SHOULD_BE_DISABLED]: true },
* { [BUTTON_CONFIG_KEYS.BUTTON_TEXT]: 'Submit', [BUTTON_CONFIG_KEYS.BUTTON_TYPE]: 'submit' }
* ]);
*
* Or using regular object keys:
* cy.validateFormFooterButtons([
* { buttonText: 'Cancel' },
* { buttonText: 'Reset', shouldBeDisabled: true },
* { buttonText: 'Submit', buttonType: 'submit' }
* ]);
*
* Both approaches work but using config-keys object(BUTTON_CONFIG_KEYS) is recommended to avoid typos and unknown keys
*/
Cypress.Commands.add('validateFormFooterButtons', (buttonConfigs) => {
if (!Array.isArray(buttonConfigs)) {
cy.logAndThrowError('buttonConfigs must be an array');
}

if (!buttonConfigs.length) {
cy.logAndThrowError('buttonConfigs array cannot be empty');
}

buttonConfigs.forEach((config) => {
validateConfigKeys(config, BUTTON_CONFIG_KEYS, 'button');

const buttonText = config[BUTTON_CONFIG_KEYS.BUTTON_TEXT];
const buttonType = config[BUTTON_CONFIG_KEYS.BUTTON_TYPE] || 'button';
const shouldBeDisabled =
config[BUTTON_CONFIG_KEYS.SHOULD_BE_DISABLED] || false;

if (!buttonText) {
cy.logAndThrowError(
`${BUTTON_CONFIG_KEYS.BUTTON_TEXT} is required for each button config`
);
}

const buttonCheck = cy
.getFormFooterButtonByTypeWithText({
buttonText,
buttonType,
})
.should('be.visible');

if (shouldBeDisabled) {
buttonCheck.and('be.disabled');
} else {
buttonCheck.and('be.enabled');
}
});
});
1 change: 1 addition & 0 deletions cypress/support/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import './commands/api_commands.js';
import './commands/custom_logging_commands.js';
import './commands/element_selectors.js';
import './commands/explorer.js';
import './commands/form_elements_validation_commands.js';
import './commands/gtl.js';
import './commands/login.js';
import './commands/menu.js';
Expand Down