Skip to content

Conversation

asirvadAbrahamVarghese
Copy link
Contributor

@asirvadAbrahamVarghese asirvadAbrahamVarghese commented Aug 8, 2025

PR to refactor schedule form cypress test

  • Use form element selector commands
  • Replace accordionItem with selectAccordionItem command
  • Other minor code improvements

@miq-bot assign @GilbertCherrie
@miq-bot add-label cypress
@miq-bot add-label test
@miq-bot add-label refactoring

@jrafanie
Copy link
Member

jrafanie commented Aug 8, 2025

we're red based on the prior change on master... as soon as that's fixed, this should be ready for re-review.

@asirvadAbrahamVarghese asirvadAbrahamVarghese force-pushed the schedule-form-cypress-refactor branch 4 times, most recently from 8cef813 to 049d7f1 Compare August 13, 2025 06:03
@jrafanie
Copy link
Member

Close / open after revert of #9505 via #9553

@jrafanie jrafanie closed this Aug 13, 2025
@jrafanie jrafanie reopened this Aug 13, 2025
@asirvadAbrahamVarghese asirvadAbrahamVarghese force-pushed the schedule-form-cypress-refactor branch from 049d7f1 to 35e263c Compare August 14, 2025 10:18
@asirvadAbrahamVarghese asirvadAbrahamVarghese force-pushed the schedule-form-cypress-refactor branch 5 times, most recently from 77cea7d to a410731 Compare August 19, 2025 09:09
// Confirms Save button is enabled after making edits
cy.contains('#main-content .bx--btn-set button[type="submit"]', saveButton)
cy.getFormFooterButtonByType(saveButton, 'submit')
Copy link
Member

Choose a reason for hiding this comment

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

saveButton is really saveButtonText and not the type. It feels like these arguments are backwards.

I would expect the method to be named and called like this:

cy.getFormFooterButtonByTypeWithText('submit', SAVE_BUTTON_TEXT)

If you don't provide a SAVE_BUTTON_TEXT, it just looking for the form button by type.

Perhaps you could have getFormFooterButtonByType and getFormFooterButtonByTypeWithText and have getFormFooterButtonByType call getFormFooterButtonByTypeWithText with no text... and have the latter function default to not checking the button text if not provided. This can be cleaned up later.

Feel free to interpret my ideas in a different way. Ultimately, the args seem backwards so that's the problem I see here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It feels like these arguments are backwards.

As it stands, 'type' defaults to 'button', putting it first forces us to provide it in every usage effectively nullifying the benefit of having a default value:
cy. getFormFooterButtonByType('button', 'Cancel');
cy. getFormFooterButtonByType('button', 'Reset');
To actually use the default value, we'd have to call the command in an awkward way like this:
cy. getFormFooterButtonByType(undefined, 'Save');

If the goal is to improve readability, I’d prefer passing an object like the one below, the keys clearly indicate what each value represents:

Cypress.Commands.add(
  'getFormFooterButtonByType',
  ({ buttonType = 'button', buttonName } = {}) =>
    cy.contains(
      `#main-content .bx--btn-set button[type="${buttonType}"]`,
      buttonName
    )
);

or we could go with the more meaningful name you suggested:

Cypress.Commands.add(
  'getFormFooterButtonByTypeWithText',
  ({ buttonType = 'button', buttonName } = {}) =>
    cy.contains(
      `#main-content .bx--btn-set button[type="${buttonType}"]`,
      buttonName
    )
);

Perhaps you could have getFormFooterButtonByType and getFormFooterButtonByTypeWithText and have getFormFooterButtonByType call getFormFooterButtonByTypeWithText with no text... and have the latter function default to not checking the button text if not provided.

We created this button selector based on an earlier discussion to use .contains, since it automatically retries the query. Sorry, if we need to select by button type, we'll have to use cy.get() instead because .contains() command expects:
cy.contains(selector, content)
or
cy.contains(content)

So when no name is passed, this is how the command gets executed:
cy.contains(#main-content .bx--btn-set button[type="${type}"])
which is telling Cypress to look for an element anywhere in the DOM that contains the text #main-content .bx--btn-set button[type="button"] and won’t match anything:
image

Based on the suggestion, the ideal way to split this into two commands would be as shown below, one using cy.get() when no button text is given:

// First command that uses .contains to match button if button text is provided:
Cypress.Commands.add(
  'getFormFooterButtonByTypeWithText',
  ({ buttonType = 'button', buttonName } = {}) =>
    cy.contains(
      `#main-content .bx--btn-set button[type="${buttonType}"]`,
      buttonName
    )
);

// Second one that calls the above if buttonName is provided:
Cypress.Commands.add(
  'getFormFooterButtonByType',
  ({ buttonType = 'button', buttonName } = {}) => {
    if (buttonName) {
      return cy.getFormFooterButtonByTypeWithText({ buttonType, buttonName });
    }
    return cy.get(`#main-content .bx--btn-set button[type="${buttonType}"]`);
  }
);

If we want to do this, personally, I’d prefer a single implementation that handles both:

Cypress.Commands.add(
  'getFormFooterButton',
  ({ buttonType = 'button', buttonName } = {}) =>
    buttonName
      ? cy.contains(
          `#main-content .bx--btn-set button[type="${buttonType}"]`,
          buttonName
        )
      : cy.get(`#main-content .bx--btn-set button[type="${buttonType}"]`)
);

The issue with the above implementation 🔝 is that when no button text is provided, and there are multiple buttons of the same type in the DOM, Cypress will throw an error when chaining further actions like .click() due to multiple matches. So selecting buttons by type alone can be unreliable:
image

Ultimately, I believe the best approach is to require button text and throw an error when it’s missing:

Cypress.Commands.add(
  'getFormFooterButtonByTypeWithText',
  ({ buttonType = 'button', buttonName } = {}) => {
    if (buttonName) {
      return cy.contains(
        `#main-content .bx--btn-set button[type="${buttonType}"]`,
        buttonName
      );
    }
    cy.logAndThrowError('buttonName is required');
  }
);

This can be cleaned up later.

It's best to finalize method and argument names early on, before they’re used widely across tests, changing them later would likely require widespread updates across test suites

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the deep dive. I agree with your conclusion after you described it. I do recall having issues where multiple dom elements match so maybe it is best to require the button text and if the type isn't button to provide both the type and the text of the button.

I'm not sure how much we'd have to change to do this.

It's best to finalize method and argument names early on, before they’re used widely across tests, changing them later would likely require widespread updates across test suites

I agree. The difficulty is making sure we've seen enough examples to be able to extract patterns. It's ok to say "we need more information or examples to refactor it". Waiting too long, yes, is also bad. It's a fine line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Opened a PR to include this change 🔝 and applied similar changes to other selectors: #9598

@asirvadAbrahamVarghese asirvadAbrahamVarghese changed the title Schedule form cypress refactor [WIP] - Schedule form cypress refactor Aug 20, 2025
@asirvadAbrahamVarghese asirvadAbrahamVarghese force-pushed the schedule-form-cypress-refactor branch 4 times, most recently from 41f59ff to ec95d4a Compare August 22, 2025 05:08
cy.getFormInputFieldById('name').type(INITIAL_SCHEDULE_NAME);
cy.getFormInputFieldById('description').type(INITIAL_DESCRIPTION);
cy.getFormInputFieldById('enabled', 'checkbox').check({
force: true,
Copy link
Member

Choose a reason for hiding this comment

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

Do we still need the force: true? We did in the past when the debug UI mode would show debug warnings that covered DOM elements on the screen as the default for cypress is to not interact with covered elements.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I forgot to say, we don't do the debug warnings with CYPRESS Env variable set to true... which is the default in CI.

Copy link
Contributor Author

@asirvadAbrahamVarghese asirvadAbrahamVarghese Aug 27, 2025

Choose a reason for hiding this comment

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

Thanks, I wanted to follow up on this but missed it

we don't do the debug warnings with CYPRESS Env variable set to true... which is the default in CI.

yep, I’m using the same in local

Problem:

Even though the checkbox was fully visible, Cypress kept throwing a "covered by another element" error
image
But was available in the DOM so force:true worked
image

The problem here was that the input type checkbox was visually hidden with style(and is covered by the label element) and user interacts with label's ::before pseudo-element
image

Resolution:

so firing the click on the label is what we want here see


/* ===== Editing a schedule ===== */
// Selecting the schedule and intercepting the API call to get schedule details
interceptGetScheduleDetailsApi();
selectConfigMenu(editScheduleConfigOption);
clickScheduleItem(INITIAL_SCHEDULE_NAME);
Copy link
Member

Choose a reason for hiding this comment

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

From a flow perspective, would it make sense to have addSchedule() return the value created, INITIAL_SCHEDULE_NAME, so it's clearer that we're clicking what was just created?

Copy link
Member

Choose a reason for hiding this comment

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

in other words:

schedule_name = addSchedule();
...
clickScheduleItem(schedule_name);
...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't directly return values using standard JavaScript return statements because Cypress commands are asynchronous and operate on a command queue
Instead we can return a cy.then() at the end to return the schedule name which will ensure all previous Cypress commands complete before returning, also allows for a clean, chainable usage pattern with .then(), likeaddSchedule().then((createdScheduleName) => {... - see

/* ===== Checking whether Reset button works ===== */
// Selecting the schedule and intercepting the API call to get schedule details
interceptGetScheduleDetailsApi();
selectConfigMenu(editScheduleConfigOption);
clickScheduleItem(INITIAL_SCHEDULE_NAME);
Copy link
Member

Choose a reason for hiding this comment

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

same as above...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ac04e8b Done in this commit

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alternatively, we can also make use of the onApiResponse callback we have in interceptApi command

@asirvadAbrahamVarghese asirvadAbrahamVarghese force-pushed the schedule-form-cypress-refactor branch from 84a923d to ac04e8b Compare August 27, 2025 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants