Cypress in manageiq-ui-classic
Setup
Initial Setup (One-Time)
cd manageiq-ui-classic
yarn # Install Cypress and dependencies (run once initially, then again when packages are updated)
Database Requirements (One-Time Setup)
##### Database Requirements
Cypress uses the development database from `config/database.yml` and expects a clean, seeded database.
1. Configure a separate database in ManageIQ `config/database.yml` under `development`, or use your existing development database. If you use your existing database, these setup steps will completely erase it.
```yaml
development:
# database: vmdb_development # Your regular dev database with data
database: vmdb_cypress # Clean database for Cypress tests
```
2. Set up the database Cypress will use:
```bash
# From manageiq directory
bundle exec rake evm:db:reset # Drops, creates, and migrates the current development database from config/database.yml
bundle exec rake db:seed # Populates default data
```
3. If you switch to a different development database later, update `config/database.yml`, then run:
```bash
bin/update # Updates dependencies and runs migrations
```
Then restart your server.
##### Resetting the Cypress Database
If you need to reset your Cypress database back to default (e.g., you added test data and want to start fresh):
```bash
# From manageiq directory, with server stopped
bundle exec rake evm:db:reset # Drops, creates, and migrates current RAILS_ENV database (development by default)
bundle exec rake db:seed # Populates default data
```
Then restart your server.
Before Running Tests
Build webpack with the CYPRESS flag (required before running tests, and whenever UI files change):
cd manageiq-ui-classic
CYPRESS=true bin/webpack
Webpack Options
- Use
CYPRESS=true bin/webpack for a one-time build
- Use
CYPRESS=true bin/webpack --watch for automatic rebuilds when editing UI files
Note: If you skip this step, Cypress will show an error and refuse to start.
Usage
Environment Variables
Required
CYPRESS=true - disables debug notifications that would prevent Cypress from accessing UI elements, development mode code reloading, and rate limiting
Optional
HEADED=true - Run with visible browser (default: headless)
SPEC="**/reports.cy.js" - Run specific test file (default: all tests)
CYPRESS_BROWSER=chromium|edge|firefox - Run with alternative browser (default: chrome)
Running Tests: Self-contained
Fully automated - no other processes needed. The rake task automatically handles starting the Rails server and simulating the queue worker.
[HEADED=true] [SPEC="**/reports.cy.js"] [CYPRESS_BROWSER=chromium|edge|firefox] CYPRESS=true bundle exec rake spec:cypress
Running Tests: Manual server
Non-interactive but requires separate Rails server (and optionally Rails console with simulated queue worker for some tests).
Start Rails server in separate terminal:
Optional: Start queue worker simulation in another terminal (needed for some tests):
bundle exec rake app:evm:simulate_queue_worker # from manageiq-ui-classic directory
# OR
bundle exec rake evm:simulate_queue_worker # from manageiq directory
Run tests with optional HEADED and SPEC parameters using Chrome (default):
[HEADED=true] [SPEC="**/reports.cy.js"] CYPRESS=true yarn cypress:run:chrome
Or use alternative browsers (chromium, edge, firefox):
[HEADED=true] [SPEC="**/reports.cy.js"] CYPRESS=true yarn cypress:run:chromium
[HEADED=true] [SPEC="**/reports.cy.js"] CYPRESS=true yarn cypress:run:edge
[HEADED=true] [SPEC="**/reports.cy.js"] CYPRESS=true yarn cypress:run:firefox
Running Tests: Interactive UI
Run tests interactively with the Cypress UI (useful for debugging).
Terminal 1 - Start webpack with –watch for live UI updates:
CYPRESS=true bin/webpack --watch
Terminal 2 - Start Rails server:
Terminal 3 - Simulate queue worker (needed for some tests):
bundle exec rake app:evm:simulate_queue_worker # from manageiq-ui-classic directory
# OR
bundle exec rake evm:simulate_queue_worker # from manageiq directory
Terminal 4 - Open Cypress interactive UI:
CYPRESS=true yarn cypress:open
This opens the Cypress UI. From there:
- Select “E2E Testing”
- Choose your browser (Chrome recommended for development)
- Click on a spec file to run it
- Watch tests run in real time with:
- Left side: test results with pass/fail status
- Right side: live browser view of the application
- Top bar: controls to pause, rerun, and see pass/fail counts
Note: Without --watch, you can run webpack and Cypress UI in the same terminal.
Tip
It’s good practice to run all commands from the manageiq-ui-classic directory. While bin/rails s can be run from the manageiq directory, commands like bin/webpack and Cypress commands only work from manageiq-ui-classic. Running everything from one location helps keep organized.
Debugging Configuration
### Debugging Configuration
#### Memory and Snapshot History
The `cypress.config.js` file contains `numTestsKeptInMemory: 0` to prevent memory issues with large test files (like `menu.cy.js` which visits every page in the UI). However, this prevents viewing snapshot history when debugging.
To enable snapshot history for easier debugging:
- Comment out the line: `// numTestsKeptInMemory: 0`
- Or change to a value > 0: `numTestsKeptInMemory: 50`
Remember to reset this before committing if you're working on large test files.
Writing Tests
Important Files
#### Important Files
Understanding these files will help you write and debug Cypress tests:
#### `cypress.config.js`
- Contains Cypress configuration settings
- Defines base URL, viewport size, video recording settings
- Controls `numTestsKeptInMemory` for debugging vs. performance
#### `cypress/support/e2e.js`
- Imports all Cypress commands and assertions
- Contains global error handling logic
- Example: Handles `uncaught:exception` errors that don't affect tests but would cause false failures in certain browsers
#### `cypress/support/assertions/`
- Contains reusable test assertion functions
- Use these to verify expected UI behavior
- Example: `cy.expect_text(element, text)` verifies element contains expected text
- Think of assertions as "test case commands" that verify conditions
#### `cypress/support/commands/`
- Contains reusable Cypress commands for common UI interactions
- Use these to navigate, click, read data, etc.
- Example: `cy.login()`, `cy.menu()`, `cy.toolbar()`
- Think of commands as "UI interaction helpers" that aren't tests themselves
Actual tests can be found in `cypress/e2e/ui/` in the [manageiq-ui-classic repository](https://github.com/ManageIQ/manageiq-ui-classic/tree/master/cypress/e2e/ui).
ManageIQ implements the following cypress extensions:
Cypress Commands (API Reference)
### Cypress Commands
#### explorer
* `cy.accordion(title)` - open an accordion panel. `title`: String for the accordion title for the accordion panel to open.
* `cy.accordionItem(name)` - click on a record in the accordion panel. `name`: String for the record to click in the accordion panel.
* `cy.selectAccordionItem(accordionPath)` - navigates the expanded accordion panel(use cy.accordion to expand an accordion panel) and then expand the nodes along the given path and click the final target item. `accordionPath`: A mixed array of strings and/or regex patterns that represent the path to the intended target node. e.g. Simple string path: `cy.selectAccordionItem(['Datastore', 'My-Domain', 'My-Namespace']);`, Path with regular expressions: `cy.selectAccordionItem([/^ManageIQ Region:/, /^Zone:/, /^Server:/]);`, Mixed path with strings and regular expressions: `cy.selectAccordionItem([/^ManageIQ Region:/, 'Zones', /^Zone:/]);`
#### gtl
* `cy.gtl_error()` - check that error message is present.
* `cy.gtlGetTable()` - returns GTL table.
* `cy.gtlGetRows(columns)` - return GTL table row data in an array. `columns`: Array of 0-based indexes of the columns to read (e.g. [1, 2, 3] will return all row data from columns 1, 2, and 3).
* `cy.gtlClickRow(columns)` - click on a row in a GTL table. `columns`: Array of `{ title: String, number: Integer }`. `title` is the string you want to find in the table to click on, `number` is the column that string is found in. (e.g. `[{title: 'Default', number: 1}, {title: 'Compute', number: 2}]` will click on a row in the GTL table with `Default` in column 1 and `Compute` in column 2. Using just `[{title: 'Default', number: 1}]` will click on the first row found in the GTL table with `Default` in column 1).
#### login
* `cy.login(user = admin, password = smartvm)` - log in to ManageIQ with the provided username and password. `user`: String for the user account to log in to, default is `admin`. `password`: String for the user account password to log in with, default is `smartvm`.
#### menu
* `cy.menu('primaryMenu', 'secondaryMenu', 'tertiaryMenu')` - navigates the side bar menu items. `primaryMenu`: String for the outer menu item on the side bar. `secondaryMenu`: String for the secondary menu when a side bar menu item is clicked. `tertiaryMenu`: String (optional) for the tertiary menu when a side bar secondary item is clicked. (e.g. `cy.menu('Overview', 'Dashboard')` will navigate to the Overview > Dashboard page while `cy.menu('Overview', 'Chargeback', 'Rates')` will navigate to the Overview > Chargeback > Rates page).
* `cy.menuItems()` - returns an Array of `{ title: String, items: Array of { title: String, href: String, items: Array of { title: String, href: String } }}` for the menu items on the side bar. `title`: String for the menu item title. `href`: String for the url to navigate to, included when the menu item has no children. `items`: Array of the same object with `title` and `href`/`items`, this is included when the menu item has children menu items.
#### miq_data_table_commands
* `cy.selectTableRowsByText({ textArray })` - selects table rows that contain any of the specified text values. Iterates through each text in the array and finds the corresponding row. If any text is not found in the table, it throws an error immediately. `textArray` is an array of text values to match against table rows. e.g. `cy.selectTableRowsByText({ textArray: ['Option 1', 'Option 2'] });`
* `cy.clickTableRowByText({ text, columnIndex })` - clicks on a table row that contains the specified text. If columnIndex is provided, it will only look for the text in that specific column. `text` is the text to find in the table row. `columnIndex` is an optional 0-based index of the column to search in. e.g. `cy.clickTableRowByText({ text: 'My Service' });`, `cy.clickTableRowByText({ text: 'Active', columnIndex: 2 });`
#### tabs
* `cy.tabs({ tabLabel })` - finds a tab element within a tablist that contains the specified label text and automatically clicks it to navigate to the tab. It requires a `tabLabel` parameter and will throw an error if none is provided. `tabLabel` is the text content of the tab to select. Returns a Cypress chainable element representing the selected tab. e.g. `cy.tabs({ tabLabel: 'Collect Logs' });`, `cy.tabs({ tabLabel: 'Settings' }).then(() => { cy.get('input#name').should('be.visible'); });`
#### toolbar
* `cy.toolbarItems(toolbarButton)` - returns an array of objects {text: String, disabled: Boolean} for the toolbar dropdown buttons for when a toolbar button is clicked. `toolbarButton` is the string for the text of the toolbar button that you want to click on.
* `cy.toolbar(toolbarButton, toolbarOption, otherOptions)` - click on the toolbar button specified by the user. Can also then click on a specified dropdown option as well. `toolbarButton` is the string for the text of the toolbar button that you want to click on. `toolbarOption` is the string for the text of the toolbar dropdown option that you want to click on. `otherOptions` is an optional object with additional options: `matchedButtonIndex` (number, default: -1) to select a specific button when multiple buttons with the same text exist. Use -1 to automatically select the first enabled button, or use 0, 1, 2... to select a specific matched button by index. e.g. `cy.toolbar('Configuration', 'Add a new Report');` (auto-selects first enabled button), `cy.toolbar('Configuration', 'Add a new Report', { matchedButtonIndex: 0 });` (selects first matched button), `cy.toolbar('Configuration', 'Add a new Report', { matchedButtonIndex: 1 });` (selects second matched button).
#### api_commands
* `cy.interceptApi({ alias, method = 'POST', urlPattern, waitOnlyIfRequestIntercepted, responseInterceptor, triggerFn, onApiResponse })` - intercepts API calls and waits for them to complete. This command will: 1) Register an intercept(in method-alias format e.g. post-myApiAlias) for the given alias & URL pattern if not already registered, 2) Execute the trigger function that makes the API call, 3) Wait for the intercepted request to complete. `alias` is the string for a unique alias for this interception. `method` is the string for the HTTP method (default: 'POST'). `urlPattern` is the string or RegExp for the URL pattern to intercept. `waitOnlyIfRequestIntercepted` is a boolean that when set to true, the command will only wait for the response if the request was actually intercepted (useful for conditional API calls - default: false). `responseInterceptor` is an optional function that can modify the response before it's returned to the application, with options to stub responses (`req.reply()`), let requests go to origin (`req.continue()`), or modify origin responses (`req.continue((res) => res.send())`). e.g. `{ responseInterceptor: (req) => req.reply({ body: { customData: 'value' } }) }`, `{ responseInterceptor: (req) => req.reply({ fixture: 'users.json' }) }`, `{ responseInterceptor: (req) => req.continue((res) => { res.send(200, { modified: true }) }) }`, `triggerFn` is the function that triggers the API call. e.g. `{ triggerFn: () => { cy.get('button').click(); } }`. `onApiResponse` is an optional callback function that receives the interception object after the API call completes. Use this to perform assertions on the response, extract data, or perform additional actions based on the API result. Default is a no-op function. e.g. `{ onApiResponse: (interception) => { expect(interception.response.statusCode).to.equal(200); } }`. Usage example: `cy.interceptApi({ alias: 'getUsers', method: 'GET', urlPattern: '/api/users', triggerFn: () => cy.get('#load-users').click(), responseInterceptor: (req) => req.reply({ body: { name: "stubbed value" } }), onApiResponse: (interception) => { expect(interception.response.statusCode).to.equal(200); } });`
* `cy.getInterceptedApiAliases()` - returns the intercepted API aliases stored in Cypress environment variables.
* `cy.setInterceptedApiAlias(aliasKey, aliasValue)` - sets an intercepted API alias in the Cypress environment variables. `aliasKey` is the string for the key/name of the alias to set. `aliasValue` is an optional string for the value to store for the alias (defaults to the same as the key). e.g. `cy.setInterceptedApiAlias('getUsersApi');`, `cy.setInterceptedApiAlias('getUsersApi', 'customValue');`
* `cy.resetInterceptedApiAliases()` - resets the intercepted API aliases stored in Cypress environment variables.
#### custom_logging_commands
* `cy.logAndThrowError(messageToLog, messageToThrow)` - Logs a custom error message to Cypress log and then throws an error. `messageToLog` is the message to display in the Cypress command log. `messageToThrow` is the optional error message to throw, defaults to `messageToLog`. e.g. `cy.logAndThrowError('This is the logged message', 'This is the thrown error message');`, `cy.logAndThrowError('This is the message that gets logged and thrown');`
#### dual_list_commands
* `cy.dualListAction({ actionType, optionsToSelect })` - performs actions on a dual-list component (components with two lists where items can be moved between them). `actionType` is the type of action to perform, use values from DUAL_LIST_ACTION_TYPE: 'add' (move selected items from left to right), 'remove' (move selected items from right to left), 'add-all' (move all items from left to right), or 'remove-all' (move all items from right to left). `optionsToSelect` is an array of option texts to select (required for 'add' and 'remove' actions, not needed for 'add-all' and 'remove-all'). e.g. `cy.dualListAction({ actionType: DUAL_LIST_ACTION_TYPE.ADD, optionsToSelect: ['Option 1', 'Option 2'] });`, `cy.dualListAction({ actionType: DUAL_LIST_ACTION_TYPE.REMOVE, optionsToSelect: ['Option 3'] });`, `cy.dualListAction({ actionType: DUAL_LIST_ACTION_TYPE.ADD_ALL });`, `cy.dualListAction({ actionType: DUAL_LIST_ACTION_TYPE.REMOVE_ALL });`
#### element_selectors
* `cy.getFormButtonByTypeWithText({ buttonType, buttonText })` - retrieves a form button, often found in form footers, by its name and type. `buttonText` is the name or text content of the button. `buttonType` is the HTML button type (e.g., 'button', 'submit', 'reset'). Defaults to 'button'. e.g. `cy.getFormButtonByTypeWithText({buttonText: 'Cancel'});`, `cy.getFormButtonByTypeWithText({buttonText: 'Submit', buttonType: 'submit'});`
* `cy.getFormInputFieldByIdAndType({ inputId, inputType })` - retrieves a form input field by its ID and type. `inputId` is the ID of the input field. `inputType` is the HTML input type (e.g., 'text', 'email', 'password'). Defaults to 'text'. e.g. `cy.getFormInputFieldByIdAndType({inputId: 'name'});`, `cy.getFormInputFieldByIdAndType({inputId: 'name', inputType: 'text'});`
* `cy.getFormLabelByForAttribute({ forValue })` - retrieves a form label associated with a specific input field by its 'for' attribute. `forValue` is the value of the 'for' attribute that matches the input field's ID. e.g. `cy.getFormLabelByForAttribute({forValue: 'name'});`
* `cy.getFormToggleButtonById({ toggleId })` - retrieves a form toggle button element by its ID. `toggleId` is the ID of the toggle button. e.g. `cy.getFormToggleButtonById({toggleId: 'tenant_mapping_enabled'});`
* `cy.getFormLegendByText({ legendText })` - retrieves a form legend element by its text content. Legend elements are typically used as captions for fieldset elements in forms. `legendText` is the text content of the legend element. e.g. `cy.getFormLegendByText({legendText: 'Basic Information'});`
* `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.validateFormButtons(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.validateFormButtons([{ buttonText: 'Cancel' }, { buttonText: 'Submit', buttonType: 'submit', shouldBeDisabled: true }]);` or using constants: `cy.validateFormButtons([{ [BUTTON_CONFIG_KEYS.TEXT]: 'Cancel', [BUTTON_CONFIG_KEYS.BUTTON_WRAPPER_CLASS]: 'custom-button-wrapper' }]);`
#### provider_helper_commands
* `cy.fillProviderForm(providerConfig, nameValue, hostValue)` - fills a provider form based on provider configuration. `providerConfig` is the provider configuration object. `nameValue` is the name to use for the provider. `hostValue` is the hostname to use for the provider.
* `cy.validateProviderFormFields(providerConfig, isEdit)` - validates a provider form based on provider configuration. `providerConfig` is the provider configuration object. `isEdit` is whether the form is in edit mode.
* `cy.interceptAddProviderApi()` - This command intercepts the POST request to '/api/providers' that occurs when adding a provider. For Azure Stack providers, it allows the request to reach the server (so data is created) and forces a successful response.
* `cy.providerValidation({ stubErrorResponse, errorMessage })` - performs validation with optional error response stubbing. `stubErrorResponse` is whether to stub an error response. `errorMessage` is the error message to show.
* `generateProviderTests(providerConfig)` - generates all test suites for a provider. `providerConfig` is the provider configuration object.
Cypress Assertions (API Reference)
### Cypress Assertions
* `cy.expect_explorer_title(title)` - check that the title on an explorer screen matches the provided title. `title`: String for the title.
* `cy.expect_gtl_no_records_with_text({ containsText })` - verifies that the GTL view displays a "no records" message. Checks that the specified text is visible within the GTL view container. `containsText` is the optional text to verify in the no records message (defaults to 'No records'). e.g. `cy.expect_gtl_no_records_with_text();`, `cy.expect_gtl_no_records_with_text({ containsText: 'No items found' });`
* `cy.expect_no_search_box()` - check if no searchbox is present on the screen.
* `cy.expect_rates_table(headers, rows)` - check the values in a chargeback rate table. `headers`: Array of strings representing the headers of the table. `rows`: Array of type `[String, [...String], [...String], [...String], [...String], String]` where each index of the array represents a column in the table. The arrays within the `rows` array can be any length and represent the values in each given column, e.g. an array of `[0.0, 100.0]` in the index for the `Range Start` column would verify that the column contains two range starts with values `0.0` and `100.0`.
* `cy.expect_show_list_title(title)` - check the title on a show\_list screen matches the provided title. `title`: String for the title.
* `cy.expect_search_box()` - check if searchbox is present on the screen.
* `cy.expect_text(element, text)` - check if the text in the element found by doing cy.get on the element String matches the provided text. `element`: String for the Cypress selector to get a specific element on the screen. `text`: String for the text that should be found within the selected element.
* `cy.expect_flash(flashType, containsText)` - command to validate flash messages. `flashType` is the type of flash. It is recommended to use values from `flashClassMap`.`containsText` is the optional text that the flash-message should contain. e.g. `expect_flash(flashClassMap.warning, 'cancelled');`
* `cy.expect_browser_confirm_with_text({ confirmTriggerFn, containsText, proceed })` - command to validate browser confirm alerts. `confirmTriggerFn` is the 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. `containsText` is the optional text that the confirm alert should contain. `proceed` is the flag to determine whether to proceed with the confirm (true = OK, false = Cancel). e.g. `cy.expect_browser_confirm_with_text({containsText: 'sure to proceed?', proceed: true, confirmTriggerFn: () => { return cy.get('[data-testid="delete"]').click()}});`, `cy.expect_browser_confirm_with_text({ confirmTriggerFn: () => cy.contains('deleted').click()});`
* `cy.expect_modal({ modalHeaderText, modalContentExpectedTexts, targetFooterButtonText })` - command to validate and interact with modal dialogs. Verifies the modal content and clicks a specified button in the modal footer. `modalHeaderText` is the optional text to verify in the modal header (case insensitive). `modalContentExpectedTexts` is an optional array of text strings that should be present in the modal content (case insensitive). `targetFooterButtonText` is the text of the button in the modal footer to click (required). e.g. `cy.expect_modal({ modalHeaderText: 'Confirmation', modalContentExpectedTexts: ['you want to continue?'], targetFooterButtonText: 'Confirm' });`, `cy.expect_modal({ modalContentExpectedTexts: ['cannot be undone.', 'data will be permanently deleted.'], targetFooterButtonText: 'Cancel' });`, `cy.expect_modal({ targetFooterButtonText: 'OK' });`
* `cy.expect_inline_field_errors({ containsText })` - command to validate inline field error messages. `containsText` is the text that the error message should contain (required). e.g. `cy.expect_inline_field_errors({ containsText: 'blank' });`, `cy.expect_inline_field_errors({ containsText: 'taken' });`
* `cy.expect_dual_list({ availableItemsHeaderText, selectedItemsHeaderText, availableItems, selectedItems })` - command to test dual-list components (components with two lists where items can be moved between them). Tests all aspects including item selection, moving items between lists, and search functionality. `availableItemsHeaderText` is the optional string for the heading of the available items list. `selectedItemsHeaderText` is the optional string for the heading of the selected items list. `availableItems` is an optional array of strings representing the items initially in the available items list. `selectedItems` is an optional array of strings representing the items initially in the selected items list. At least one of `availableItems` or `selectedItems` must contain items. The command automatically detects whether to test a flow starting from available items or selected items based on which list has items initially. e.g. `cy.expect_dual_list({ availableItemsHeaderText: 'Available Items', selectedItemsHeaderText: 'Selected Items', availableItems: ['Item 1', 'Item 2', 'Item 3'] });`, `cy.expect_dual_list({ availableItemsHeaderText: 'Unassigned Roles', selectedItemsHeaderText: 'Assigned Roles', selectedItems: ['Role 1', 'Role 2', 'Role 3'] });`
Test Writing Guidelines
## Test Writing Guidelines
#### 1. Database State Management
##### Resetting Test Data
Our Cypress configuration captures the database table state (rows that exist) before all tests run. You can restore this state between tests using `cy.appDbState('restore')`:
```javascript
afterEach(() => {
cy.appDbState('restore');
});
```
What `appDbState('restore')` does:
- Removes rows created during the test - Use `afterEach` with `cy.appDbState('restore')` for tests that create new records
- Does NOT restore deleted or modified rows - If your test deletes or modifies existing rows, you must manually restore them in your test
**Examples:**
- [Tests using appDbState('restore')](https://github.com/search?q=repo%3AManageIQ%2Fmanageiq-ui-classic+appDbState%28%27restore%27%29&type=code)
##### Creating Test Data with FactoryBot
Through cypress-on-rails, you can use the Rails application's existing test factories from JavaScript using `cy.appFactories()`. Check the [existing factories](https://github.com/ManageIQ/manageiq/tree/master/spec/factories) before creating a new one - you can use them directly or create new ones based on existing ones. For more on defining and using factories, see the [FactoryBot Getting Started guide](https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED).
```javascript
// Create a single record
cy.appFactories([
['create', 'service_template', {name: 'My Service', generic_subtype: 'custom', prov_type: 'generic', display: true}],
]).then((results) => {
// results[0] contains the created record with its id
const serviceTemplate = results[0];
});
// Create related records using the first record's id
cy.appFactories([
['create', 'service_template', {name: 'My Service'}],
]).then((results) => {
cy.appFactories([
['create', 'resource_action', {action: 'Provision', resource_id: results[0].id, resource_type: 'ServiceTemplate'}],
['create', 'resource_action', {action: 'Retirement', resource_id: results[0].id, resource_type: 'ServiceTemplate'}]
]);
});
```
**Important requirements:**
- **Factory names must match existing Ruby-side factories** - The argument after 'create' (e.g., 'service_template') must correspond to a defined FactoryBot factory in the Ruby codebase
- **All factory names must be unique** - When creating new factories for Cypress tests, ensure the factory name doesn't conflict with existing factories
- **Design factories to return the needed object** - If your Cypress test needs specific information (id, name, etc.) from a created object, structure the Ruby factory so that object is the top-level return value. You may need to flip the order of how dependent associations are created in the factory
**Best practices:**
- Put complicated logic for creating records in the factory itself (in Ruby)
- Use `cy.appFactories()` to string together simple relationships in tests
**Examples:**
- [Tests using cy.appFactories()](https://github.com/search?q=repo%3AManageIQ%2Fmanageiq-ui-classic+cy.appFactories&type=code)
**Note:** Both factories and resetting test data can be used in combination with combining/splitting tests (see "Test Structure and Granularity" section below) to simplify test setup and make feature testing more readable.
#### 2. File Structure
Organize test files to match the UI navigation structure:
```
UI Navigation: Overview > Chargeback > Rates
Test File: cypress/e2e/ui/Overview/Chargeback/rates.cy.js
```
For very large test files, split them by feature or feature category so the file names describe what each test covers:
```
cypress/e2e/ui/Overview/Chargeback/Rates/rate-list.cy.js
cypress/e2e/ui/Overview/Chargeback/Rates/rate-form.cy.js
cypress/e2e/ui/Overview/Chargeback/Rates/rate-validation.cy.js
```
#### 3. No Provider Data
We currently have no way to seed real provider data to the database. This prevents testing provider-related functionality. However, many pages can be tested without provider data.
See [issue #8859](https://github.com/ManageIQ/manageiq-ui-classic/issues/8859) for a list of pages that can be tested without provider data (Phase 2 scope).
#### 4. Create Baseline Tests
For each spec file, create baseline tests that verify:
- Page loads properly
- Default data is present and correct
- Basic UI elements are visible
**Example:** In [rates.cy.js](https://github.com/ManageIQ/manageiq-ui-classic/blob/master/cypress/e2e/ui/Overview/Chargeback/rates.cy.js), baseline tests check that default rates are in the table with correct values.
#### 5. Test All Browsers
Before creating a PR, ensure your tests pass on:
- Chrome
- Edge
- Firefox
**Note:** Run tests on all browsers using the commands in the Usage section above.
#### 6. Test Structure and Granularity
Use `describe()` for organizing related tests and `it()` for individual test cases.
These are integration tests that simulate real user workflows through the UI - they're not unit tests. You'll need to decide whether to combine operations (add/edit/delete) into workflow tests or keep them separate. There are tradeoffs between test speed, test readability, and failure reporting, so weigh the pros/cons:
**Combined workflow tests:**
- Faster, simulates real user behavior
- Actions build on each other (edit and delete can use the previously added record)
- Easier to follow when setup is complex
- Less specific failures and can become long
```javascript
it('can add, edit, and delete a rate', () => {
// Add, edit, delete in one test
});
```
**Separate tests:**
- Clearer failure reporting, easier to maintain
- Slower and harder to follow (setup often in separate beforeEach blocks)
```javascript
it('can add a rate', () => { /* ... */ });
it('can edit a rate', () => { /* ... */ });
it('can delete a rate', () => { /* ... */ });
```
**Guidelines:** Start with workflow tests for happy paths, use separate tests for edge cases and validations.