Skip to content
57 changes: 57 additions & 0 deletions config/initializers/cypress_on_rails.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# This is identical to the generated file[1] with the exceptions below:
# [1] https://github.com/shakacode/cypress-playwright-on-rails/blob/ac5d69f9ea951c7545e5141db2abb2cb1350e740/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb
# * install_folder (uses our rails engine path)
# * require ENV['CYPRESS'] to be set
# * requires and instantiates the seeded_deletion strategy
if ENV['CYPRESS'].present?
CypressOnRails.configure do |c|
c.api_prefix = ""

# Currently, the only change from the template:
c.install_folder = ManageIQ::UI::Classic::Engine.root.join("cypress/e2e")
# WARNING!! CypressOnRails can execute arbitrary ruby code
# please use with extra caution if enabling on hosted servers or starting your local server on 0.0.0.0
c.use_middleware = !Rails.env.production?
# c.use_vcr_middleware = !Rails.env.production?
# # Use this if you want to use use_cassette wrapper instead of manual insert/eject
# # c.use_vcr_use_cassette_middleware = !Rails.env.production?
# # Pass custom VCR options
# c.vcr_options = {
# hook_into: :webmock,
# default_cassette_options: { record: :once },
# cassette_library_dir: File.expand_path("#{__dir__}/../../e2e/cypress/fixtures/vcr_cassettes")
# }
c.logger = Rails.logger

# Server configuration for rake tasks (cypress:open, cypress:run, playwright:open, playwright:run)
# c.server_host = 'localhost' # or use ENV['CYPRESS_RAILS_HOST']
# c.server_port = 3001 # or use ENV['CYPRESS_RAILS_PORT']
# c.transactional_server = true # Enable automatic transaction rollback between tests

# Server lifecycle hooks for rake tasks
# c.before_server_start = -> { DatabaseCleaner.clean_with(:truncation) }
# c.after_server_start = -> { puts "Test server started on port #{CypressOnRails.configuration.server_port}" }
# c.after_transaction_start = -> { Rails.application.load_seed }
# c.after_state_reset = -> { Rails.cache.clear }
# c.before_server_stop = -> { puts "Stopping test server..." }

# If you want to enable a before_request logic, such as authentication, logging, sending metrics, etc.
# Refer to https://www.rubydoc.info/gems/rack/Rack/Request for the `request` argument.
# Return nil to continue through the Cypress command. Return a response [status, header, body] to halt.
# c.before_request = lambda { |request|
# unless request.env['warden'].authenticate(:secret_key)
# return [403, {}, ["forbidden"]]
# end
# }
end

# # if you compile your assets on CI
# if ENV['CYPRESS'].present? && ENV['CI'].present?
# Rails.application.configure do
# config.assets.compile = false
# config.assets.unknown_asset_fallback = false
# end
# end
require 'extensions/database_cleaner-activerecord-seeded_deletion'
DatabaseCleaner[:active_record].strategy = DatabaseCleaner::ActiveRecord::SeededDeletion.new(:pre_count => true)
end
23 changes: 23 additions & 0 deletions cypress/e2e/app_commands/db_state.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
logger.debug "[Cypress] Running db_state #{command_options}"
option = command_options.to_s.downcase
case option
when "capture"
logger.info "[Cypress] DatabaseCleaner capturing"
DatabaseCleaner.start
when "restore"
logger.info "[Cypress] DatabaseCleaner restoring"
DatabaseCleaner.clean
CypressOnRails::SmartFactoryWrapper.reload

if defined?(VCR)
VCR.eject_cassette # make sure we no cassette inserted before the next test starts
VCR.turn_off!
WebMock.disable! if defined?(WebMock)
Copy link
Member Author

Choose a reason for hiding this comment

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

Note, I was going to put this into a class/module but it's evaluated in the context of the cypress on rails command executor so finding/prepending classes was more effort than needed. If we want to move the logic into methods, we can do that later.

end
else
message = "Unknown db_state #{option}!"
logger.error "[Cypress] #{message}"
raise message
end
# Explicitly return nil from the script so the cypress on rails middleware doesn't try to parse a response
nil
1 change: 1 addition & 0 deletions cypress/e2e/app_commands/eval.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Kernel.eval(command_options) unless command_options.nil?
12 changes: 12 additions & 0 deletions cypress/e2e/app_commands/factory_bot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Array.wrap(command_options).map do |factory_options|
factory_method = factory_options.shift
begin
logger.debug "running #{factory_method}, #{factory_options}"
Copy link
Member Author

Choose a reason for hiding this comment

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

for followup... I should use the same prefix for all logger messages such as here and below...

Copy link
Member Author

Choose a reason for hiding this comment

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

Note, this comes from the generator so we'll have to change that generated file if they make changes to it in the future.

CypressOnRails::SmartFactoryWrapper.public_send(factory_method, *factory_options)
rescue => e
logger.error "#{e.class}: #{e.message}"
logger.error e.backtrace.join("\n")
logger.error "#{e.record.inspect}" if e.is_a?(ActiveRecord::RecordInvalid)
raise e
end
end
42 changes: 42 additions & 0 deletions cypress/e2e/e2e_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# This is loaded once before the first command is executed

begin
require 'database_cleaner-active_record'
Copy link
Member Author

Choose a reason for hiding this comment

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

Note, this file is loaded differently as it's run through their executor thing. We might be able to drop many of the requires here since it's required in engine.rb. But for now, I kept it mostly like they have it in their generator.

rescue LoadError => e
puts e.message
begin
require 'database_cleaner'
rescue LoadError => e
puts e.message
end
end

begin
require 'factory_bot_rails'
rescue LoadError => e
puts e.message
begin
require 'factory_girl_rails'
rescue LoadError => e
puts e.message
end
end

require 'cypress_on_rails/smart_factory_wrapper'

factory = CypressOnRails::SimpleRailsFactory
factory = FactoryBot if defined?(FactoryBot)
factory = FactoryGirl if defined?(FactoryGirl)

# TODO: By default, Factory bot sets definition_file_paths to directories to search for factories:
# https://github.com/thoughtbot/factory_bot/blob/8446cb6c5b39ea046d8ba180197aabc66adf62ed/lib/factory_bot/find_definitions.rb#L10
# Cypress on rails SmartFactoryWrapper is expecting files to be a pattern or a file that when evaluated with Dir[xxx],
# will return files, not a directory:
# https://github.com/shakacode/cypress-playwright-on-rails/blob/abb505c0691c29d5f2a57c2ba28aedbfd43d079e/lib/cypress_on_rails/smart_factory_wrapper.rb#L88
# Therefore, to reuse the existing definition_file_paths, we must convert the directories to glob pattern matches.
require Rails.root.join('spec/support/factory_bot_helper')
CypressOnRails::SmartFactoryWrapper.configure(
always_reload: false,
factory: factory,
files: FactoryBot.definition_file_paths.flat_map { |p| p.directory? ? p.join("**/*.rb") : p }
)
53 changes: 11 additions & 42 deletions cypress/e2e/ui/Automation/Embedded-Automate/Explorer/class.cy.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,23 @@
/* eslint-disable no-undef */

describe('Automation > Embedded Automate > Explorer', () => {
before(() => {
// Create a Domain and Namespace before all the tests
beforeEach(() => {
cy.appFactories([
['create', 'miq_ae_domain', {name: 'TestDomain'}],
]).then((results) => {
cy.appFactories([
['create', 'miq_ae_namespace', {name: 'TestNameSpace', domain_id: results[0].id}]
])
});
Copy link
Member Author

Choose a reason for hiding this comment

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

This is how you can use factories in cypress to create the domain and namespace and replaces all of that code on the left hand side. If you're testing the creation in the UI, the code on the left is fine but since this is just setup code, the factories above should be used.

Copy link
Member Author

Choose a reason for hiding this comment

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

Note, this appFactories command in cypress leverages the new endpoints mentioned in the PR description, namely, the __e2e__/command endpoint.


cy.login();
cy.intercept('POST', '/ops/accordion_select?id=rbac_accord').as('accordion');
cy.menu('Automation', 'Embedded Automate', 'Explorer');
cy.get('#explorer_title_text');

// Creates a Domain
cy.get('[title="Datastore"]').click();
cy.get('[title="Configuration"]').click();
cy.get('[title="Add a New Domain"]').click();
cy.get('[name="name"]').type('TestDomain', {force: true});
cy.get('[name="description"]').type('This is a test domain');
cy.get('#enabled').check();
cy.get('[class="bx--btn bx--btn--primary"]').contains('Add').click();

// Check for the success message
cy.get('div.alert.alert-success.alert-dismissable').should('exist').and('contain', 'Automate Domain "TestDomain" was added').find('button.close').should('exist');

// Creates a Namespace
cy.get('[title="Datastore"]').click();
cy.get('[title="Automate Domain: TestDomain"]').click(); // Click on Domain
cy.get('[title="Configuration"]').click();
cy.get('[title="Add a New Namespace"]').click();
cy.get('[name="name"]').type('TestNameSpace', {force: true});
cy.get('[name="description"]').type('This is a test NameSpace');
cy.get('.bx--btn--primary').contains('Add').click();

// Wait for namespace to be visible
cy.get('[title="Automate Namespace: TestNameSpace"]', {timeout: 1000}).should('be.visible')
});

beforeEach(() => {
cy.login();
cy.intercept('POST', '/ops/accordion_select?id=rbac_accord').as('accordion');
cy.menu('Automation', 'Embedded Automate', 'Explorer');
cy.get('#explorer_title_text');
afterEach(() => {
cy.appDbState('restore');
});

describe('Class Form', () => {
Expand Down Expand Up @@ -268,15 +248,4 @@ describe('Automation > Embedded Automate > Explorer', () => {
cy.get('#ns_details_div > .alert').contains('The selected Namespace is empty');
});
});

after(() => {
// Remove the Domain after all the tests
cy.menu('Automation', 'Embedded Automate', 'Explorer');
cy.get('[title="Datastore"]').click({force: true});
cy.get('[title="Automate Domain: TestDomain"]').click({force: true});
cy.get('[title="Configuration"]').click({force: true});
cy.get('[title="Remove this Domain"]').click({force: true});

cy.get('.bx--data-table-content tbody tr').should('not.contain', 'Automate Domain: TestDomain');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -122,22 +122,23 @@ function createNamespaceAndOpenEditForm() {

describe('Automate operations on Namespaces: Automation -> Embedded Automate -> Explorer -> {Any-created-domain} -> Namespace form', () => {
beforeEach(() => {
cy.appFactories([
['create', 'miq_ae_domain', {name: DOMAIN_NAME}],
]);

cy.login();
cy.menu(
AUTOMATION_MENU_OPTION,
EMBEDDED_AUTOMATION_MENU_OPTION,
EXPLORER_MENU_OPTION
);
cy.accordion(DATA_STORE_ACCORDION_LABEL);
/* TODO: DATA_SETUP - Refactor to use API for domain data setup */
cy.toolbar(TOOLBAR_CONFIGURATION, TOOLBAR_ADD_NEW_DOMAIN);
addDomainOrNamespace({
nameFieldValue: DOMAIN_NAME,
});
cy.expect_flash(flashClassMap.success, FLASH_MESSAGE_ADD_SUCCESS);
Copy link
Member Author

Choose a reason for hiding this comment

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

I removed the setup code that navigated to the add domain screen to create it and instead used factories. I kept the other setup navigation steps.

cy.selectAccordionItem([DATA_STORE_ACCORDION_LABEL, DOMAIN_NAME]);
});

afterEach(() => {
cy.appDbState('restore');
});

it('Validate Add Namespace form fields', () => {
cy.toolbar(TOOLBAR_CONFIGURATION, TOOLBAR_ADD_NEW_NAMESPACE);
validateNamespaceFormFields();
Expand Down Expand Up @@ -245,34 +246,4 @@ describe('Automate operations on Namespaces: Automation -> Embedded Automate ->
}).click();
cy.expect_flash(flashClassMap.warning, FLASH_MESSAGE_CANCELLED);
});

afterEach(() => {
cy.url()
.then((url) => {
// Ensures navigation to Automation -> Embedded Automate -> Explorer in the UI
if (!url.endsWith(COMPONENT_ROUTE_URL)) {
cy.visit(COMPONENT_ROUTE_URL);
}
cy.accordion(DATA_STORE_ACCORDION_LABEL);
})
.then(() => {
cy.get('div.panel-collapse.collapse.in li.list-group-item').each(
(item) => {
const text = item.text().trim();
if (text === DOMAIN_NAME) {
if (!item.hasClass('node-selected')) {
cy.wrap(item).click();
}
cy.expect_browser_confirm_with_text({
confirmTriggerFn: () =>
cy.toolbar(TOOLBAR_CONFIGURATION, TOOLBAR_REMOVE_DOMAIN),
containsText: BROWSER_CONFIRM_REMOVE_MESSAGE,
});
return false; // exit iteration
}
return null; // has no impact, just to get rid of eslint warning
}
);
});
Copy link
Member Author

Choose a reason for hiding this comment

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

🎉

});
});
32 changes: 1 addition & 31 deletions cypress/e2e/ui/Settings/Application-Settings/schedule.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,36 +534,6 @@ describe('Automate Schedule form operations: Settings > Application Settings > S
});

afterEach(() => {
cy.url()
.then((url) => {
// Ensures navigation to Settings -> Application-Settings in the UI
if (!url.endsWith(COMPONENT_ROUTE_URL)) {
cy.visit(COMPONENT_ROUTE_URL);
}
cy.accordion(SETTINGS_OPTION);
})
.then(() => {
// Iterate and clean up any leftover schedules created during the test
cy.get('div.panel-collapse.collapse.in li.list-group-item').each(
(item) => {
const text = item.text().trim();
if (
text === INITIAL_SCHEDULE_NAME ||
text === EDITED_SCHEDULE_NAME
) {
if (!item.hasClass('node-selected')) {
cy.wrap(item).click();
}
cy.expect_browser_confirm_with_text({
confirmTriggerFn: () =>
selectConfigMenu(DELETE_SCHEDULE_CONFIG_OPTION),
containsText: BROWSER_ALERT_DELETE_CONFIRM_TEXT,
});
return false; // exit iteration
}
return null; // has no impact, just to get rid of eslint warning
}
);
});
Copy link
Member Author

Choose a reason for hiding this comment

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

🥳

cy.appDbState('restore');
});
});
Loading