diff --git a/CHANGELOG.md b/CHANGELOG.md index 74dd8a6..2704788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,82 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed +* **BREAKING: Generator folder structure**: Fixed install generator to create `e2e_helper.rb` and `app_commands/` at the install folder root (e.g., `e2e/`) instead of inside the framework subdirectory (e.g., `e2e/cypress/`). This ensures compatibility between Cypress/Playwright config file location and middleware expectations. [#201] + +### Migration Guide for Folder Structure Change + +#### Breaking Change Notice +If you previously ran the install generator (versions prior to 1.20.0), the file structure created was incorrect. The generator placed helper and command files in the framework subdirectory when they should be at the install folder root. + +**Old (Incorrect) Structure:** +``` +e2e/ + cypress.config.js + cypress/ + e2e_helper.rb ← Wrong location + app_commands/ ← Wrong location + support/ + e2e/ +``` + +**New (Correct) Structure:** +``` +e2e/ + cypress.config.js + e2e_helper.rb ← Correct location + app_commands/ ← Correct location + fixtures/ + vcr_cassettes/ ← VCR cassettes also at root + cypress/ + support/ + e2e/ +``` + +#### How to Migrate + +**Option 1: Fresh Installation (Recommended for new projects)** +```bash +# Remove old structure +rm -rf e2e/ + +# Re-run generator +bin/rails g cypress_on_rails:install --force +``` + +**Option 2: Manual Migration (For existing projects with custom code)** +```bash +# Move files to correct location +mv e2e/cypress/e2e_helper.rb e2e/ +mv e2e/cypress/app_commands e2e/ + +# Update VCR cassettes path if using VCR +mv e2e/cypress/fixtures e2e/ + +# Verify your initializer has the correct path +# Should be: c.install_folder = File.expand_path("#{__dir__}/../../e2e") +# Not: c.install_folder = File.expand_path("#{__dir__}/../../e2e/cypress") +``` + +**Option 3: Update Initializer Only (Quick fix, not recommended)** +If you cannot migrate files immediately, you can temporarily update your initializer: +```ruby +# config/initializers/cypress_on_rails.rb +c.install_folder = File.expand_path("#{__dir__}/../../e2e/cypress") +``` +However, this means Cypress may have issues finding config files. We recommend migrating to the correct structure. + +#### Why This Change? +The middleware expects to find `e2e_helper.rb` at `#{install_folder}/e2e_helper.rb` and commands at `#{install_folder}/app_commands/`. Meanwhile, Cypress/Playwright expect config files at the install_folder root when using `--project` flag. The previous structure created a conflict where these requirements couldn't both be satisfied. + +#### Testing Your Migration +After migrating, verify: +1. Run `bin/rails cypress:open` or `bin/rails playwright:open` - should open successfully +2. Run a test that uses app commands - should execute without "file not found" errors +3. Check that VCR cassettes (if used) are being created/loaded correctly + +--- + ## [1.19.0] - 2025-10-01 ### Added diff --git a/README.md b/README.md index 064c0e2..a747b8d 100644 --- a/README.md +++ b/README.md @@ -134,17 +134,52 @@ bin/rails g cypress_on_rails:install --install_with=skip bin/rails g cypress_on_rails:install --install_with=skip ``` -The generator modifies/adds the following files/directory in your application: -* `config/initializers/cypress_on_rails.rb` used to configure Cypress on Rails -* `e2e/cypress/integration/` contains your cypress tests -* `e2e/cypress/support/on-rails.js` contains Cypress on Rails support code -* `e2e/cypress/e2e_helper.rb` contains helper code to require libraries like factory_bot -* `e2e/cypress/app_commands/` contains your scenario definitions -* `e2e/playwright/e2e/` contains your playwright tests -* `e2e/playwright/support/on-rails.js` contains Playwright on Rails support code - -If you are not using `database_cleaner` look at `e2e/cypress/app_commands/clean.rb`. -If you are not using `factory_bot` look at `e2e/cypress/app_commands/factory_bot.rb`. +The generator creates the following structure in your application: + +**For Cypress:** +``` +e2e/ + cypress.config.js # Cypress configuration + e2e_helper.rb # Helper code for factory_bot, database_cleaner, etc. + app_commands/ # Your custom commands and scenarios + clean.rb + factory_bot.rb + scenarios/ + basic.rb + fixtures/ + vcr_cassettes/ # VCR recordings (if using VCR) + cypress/ + support/ + index.js + commands.js + on-rails.js # Cypress on Rails support code + e2e/ + rails_examples/ # Example tests +``` + +**For Playwright:** +``` +e2e/ + playwright.config.js # Playwright configuration + e2e_helper.rb # Helper code for factory_bot, database_cleaner, etc. + app_commands/ # Your custom commands and scenarios (shared with Cypress) + fixtures/ + vcr_cassettes/ # VCR recordings (if using VCR) + playwright/ + support/ + index.js + on-rails.js # Playwright on Rails support code + e2e/ + rails_examples/ # Example tests +``` + +**Additional files:** +* `config/initializers/cypress_on_rails.rb` - Configuration for Cypress on Rails + +**Important:** Note that `e2e_helper.rb` and `app_commands/` are at the root of the install folder (e.g., `e2e/`), NOT inside the framework subdirectory (e.g., `e2e/cypress/`). This allows both Cypress and Playwright to share the same commands and helpers when using both frameworks. + +If you are not using `database_cleaner` look at `e2e/app_commands/clean.rb`. +If you are not using `factory_bot` look at `e2e/app_commands/factory_bot.rb`. Now you can create scenarios and commands that are plain Ruby files that get loaded through middleware, the ruby sky is your limit. @@ -448,8 +483,7 @@ Add your VCR configuration to your `config/cypress_on_rails.rb` c.vcr_options = { hook_into: :webmock, default_cassette_options: { record: :once }, - # It's possible to override cassette_library_dir using install_folder - cassette_library_dir: File.expand_path("#{__dir__}/../../spec/cypress/fixtures/vcr_cassettes") + cassette_library_dir: File.expand_path("#{__dir__}/../../e2e/fixtures/vcr_cassettes") } ``` diff --git a/lib/cypress_on_rails/command_executor.rb b/lib/cypress_on_rails/command_executor.rb index 3af0605..af93ed2 100644 --- a/lib/cypress_on_rails/command_executor.rb +++ b/lib/cypress_on_rails/command_executor.rb @@ -16,6 +16,26 @@ def self.perform(file,command_options = nil) def self.load_e2e_helper e2e_helper_file = "#{configuration.install_folder}/e2e_helper.rb" cypress_helper_file = "#{configuration.install_folder}/cypress_helper.rb" + + # Check for old structure (files in framework subdirectory) + old_cypress_location = "#{configuration.install_folder}/cypress/e2e_helper.rb" + old_playwright_location = "#{configuration.install_folder}/playwright/e2e_helper.rb" + + if File.exist?(old_cypress_location) || File.exist?(old_playwright_location) + old_location = File.exist?(old_cypress_location) ? old_cypress_location : old_playwright_location + logger.warn "=" * 80 + logger.warn "DEPRECATION WARNING: Old folder structure detected!" + logger.warn "Found e2e_helper.rb at: #{old_location}" + logger.warn "This file should be at: #{e2e_helper_file}" + logger.warn "" + logger.warn "The generator now creates e2e_helper.rb and app_commands/ at the install_folder" + logger.warn "root, not inside the framework subdirectory." + logger.warn "" + logger.warn "To fix this, run: mv #{old_location} #{e2e_helper_file}" + logger.warn "See CHANGELOG.md for full migration guide." + logger.warn "=" * 80 + end + if File.exist?(e2e_helper_file) Kernel.require e2e_helper_file elsif File.exist?(cypress_helper_file) diff --git a/lib/generators/cypress_on_rails/install_generator.rb b/lib/generators/cypress_on_rails/install_generator.rb index 0a63f28..b5d2dab 100644 --- a/lib/generators/cypress_on_rails/install_generator.rb +++ b/lib/generators/cypress_on_rails/install_generator.rb @@ -42,8 +42,8 @@ def install_framework def add_initial_files template "config/initializers/cypress_on_rails.rb.erb", "config/initializers/cypress_on_rails.rb" - template "spec/e2e/e2e_helper.rb.erb", "#{options.install_folder}/#{options.framework}/e2e_helper.rb" - directory 'spec/e2e/app_commands', "#{options.install_folder}/#{options.framework}/app_commands" + template "spec/e2e/e2e_helper.rb.erb", "#{options.install_folder}/e2e_helper.rb" + directory 'spec/e2e/app_commands', "#{options.install_folder}/app_commands" if options.framework == 'cypress' copy_file "spec/cypress/support/on-rails.js", "#{options.install_folder}/cypress/support/on-rails.js" directory 'spec/cypress/e2e/rails_examples', "#{options.install_folder}/cypress/e2e/rails_examples" diff --git a/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb b/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb index afc3a22..410d483 100644 --- a/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb +++ b/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb @@ -1,7 +1,7 @@ if defined?(CypressOnRails) CypressOnRails.configure do |c| c.api_prefix = "<%= options.api_prefix %>" - c.install_folder = File.expand_path("#{__dir__}/../../<%= options.install_folder %>/<%= options.framework %>") + c.install_folder = File.expand_path("#{__dir__}/../../<%= options.install_folder %>") # 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? @@ -12,7 +12,7 @@ if defined?(CypressOnRails) <% unless options.experimental %># <% end %> c.vcr_options = { <% unless options.experimental %># <% end %> hook_into: :webmock, <% unless options.experimental %># <% end %> default_cassette_options: { record: :once }, - <% unless options.experimental %># <% end %> cassette_library_dir: File.expand_path("#{__dir__}/../../<%= options.install_folder %>/<%= options.framework %>/fixtures/vcr_cassettes") + <% unless options.experimental %># <% end %> cassette_library_dir: File.expand_path("#{__dir__}/../../<%= options.install_folder %>/fixtures/vcr_cassettes") <% unless options.experimental %># <% end %> } c.logger = Rails.logger diff --git a/spec/generators/install_generator_spec.rb b/spec/generators/install_generator_spec.rb new file mode 100644 index 0000000..731e013 --- /dev/null +++ b/spec/generators/install_generator_spec.rb @@ -0,0 +1,199 @@ +require 'spec_helper' +require 'rails/generators' +require 'generators/cypress_on_rails/install_generator' +require 'tmpdir' +require 'fileutils' + +RSpec.describe CypressOnRails::InstallGenerator, type: :generator do + let(:destination_root) { Dir.mktmpdir } + + before do + # Set up a minimal Rails app structure + FileUtils.mkdir_p(File.join(destination_root, 'config', 'initializers')) + FileUtils.mkdir_p(File.join(destination_root, 'bin')) + + # Mock the generator's destination_root + allow(Dir).to receive(:pwd).and_return(destination_root) + + # Prevent actual npm/yarn installation in tests + allow_any_instance_of(CypressOnRails::InstallGenerator).to receive(:system).and_return(true) + end + + after do + FileUtils.rm_rf(destination_root) + end + + describe 'with default options (cypress framework, e2e folder)' do + let(:args) { [] } + let(:options) { {} } + + before do + run_generator(args, options) + end + + it 'creates the initializer with correct install_folder path' do + initializer_path = File.join(destination_root, 'config', 'initializers', 'cypress_on_rails.rb') + expect(File).to exist(initializer_path) + + content = File.read(initializer_path) + # Should point to e2e, not e2e/cypress + expect(content).to include('c.install_folder = File.expand_path("#{__dir__}/../../e2e")') + end + + it 'creates cypress config at install_folder root' do + config_path = File.join(destination_root, 'e2e', 'cypress.config.js') + expect(File).to exist(config_path) + end + + it 'creates e2e_helper.rb at install_folder root' do + helper_path = File.join(destination_root, 'e2e', 'e2e_helper.rb') + expect(File).to exist(helper_path) + end + + it 'creates app_commands directory at install_folder root' do + commands_path = File.join(destination_root, 'e2e', 'app_commands') + expect(File).to be_directory(commands_path) + end + + it 'creates cypress support files in framework subdirectory' do + support_path = File.join(destination_root, 'e2e', 'cypress', 'support', 'index.js') + expect(File).to exist(support_path) + end + + it 'creates cypress examples in framework subdirectory' do + examples_path = File.join(destination_root, 'e2e', 'cypress', 'e2e', 'rails_examples') + expect(File).to be_directory(examples_path) + end + end + + describe 'with playwright framework' do + let(:args) { [] } + let(:options) { { framework: 'playwright' } } + + before do + run_generator(args, options) + end + + it 'creates the initializer with correct install_folder path' do + initializer_path = File.join(destination_root, 'config', 'initializers', 'cypress_on_rails.rb') + expect(File).to exist(initializer_path) + + content = File.read(initializer_path) + # Should point to e2e, not e2e/playwright + expect(content).to include('c.install_folder = File.expand_path("#{__dir__}/../../e2e")') + end + + it 'creates playwright config at install_folder root' do + config_path = File.join(destination_root, 'e2e', 'playwright.config.js') + expect(File).to exist(config_path) + end + + it 'creates e2e_helper.rb at install_folder root' do + helper_path = File.join(destination_root, 'e2e', 'e2e_helper.rb') + expect(File).to exist(helper_path) + end + + it 'creates app_commands directory at install_folder root' do + commands_path = File.join(destination_root, 'e2e', 'app_commands') + expect(File).to be_directory(commands_path) + end + + it 'creates playwright support files in framework subdirectory' do + support_path = File.join(destination_root, 'e2e', 'playwright', 'support', 'index.js') + expect(File).to exist(support_path) + end + end + + describe 'with custom install_folder' do + let(:args) { [] } + let(:options) { { install_folder: 'spec/system' } } + + before do + run_generator(args, options) + end + + it 'creates files in the custom folder' do + helper_path = File.join(destination_root, 'spec', 'system', 'e2e_helper.rb') + expect(File).to exist(helper_path) + + commands_path = File.join(destination_root, 'spec', 'system', 'app_commands') + expect(File).to be_directory(commands_path) + end + + it 'sets the correct install_folder in the initializer' do + initializer_path = File.join(destination_root, 'config', 'initializers', 'cypress_on_rails.rb') + content = File.read(initializer_path) + expect(content).to include('c.install_folder = File.expand_path("#{__dir__}/../../spec/system")') + end + end + + describe 'file structure ensures middleware and framework compatibility' do + let(:args) { [] } + let(:options) { {} } + + before do + run_generator(args, options) + end + + it 'places e2e_helper.rb where middleware expects it (install_folder/e2e_helper.rb)' do + # Middleware looks for #{install_folder}/e2e_helper.rb + helper_path = File.join(destination_root, 'e2e', 'e2e_helper.rb') + expect(File).to exist(helper_path) + expect(File.read(helper_path)).to include('CypressOnRails') + end + + it 'places app_commands where middleware expects it (install_folder/app_commands)' do + # Middleware looks for #{install_folder}/app_commands/#{command}.rb + commands_path = File.join(destination_root, 'e2e', 'app_commands') + expect(File).to be_directory(commands_path) + + # Check that command files exist + clean_cmd = File.join(commands_path, 'clean.rb') + expect(File).to exist(clean_cmd) + end + + it 'places cypress.config.js where cypress --project flag expects it' do + # Cypress runs with --project install_folder, expects config at that level + config_path = File.join(destination_root, 'e2e', 'cypress.config.js') + expect(File).to exist(config_path) + + # Verify the config references the correct relative path for support files + content = File.read(config_path) + expect(content).to include('cypress/support/index.js') + end + + it 'creates a valid directory structure' do + # The expected structure: + # e2e/ + # cypress.config.js <- Config at root of install_folder + # e2e_helper.rb <- Helper at root of install_folder + # app_commands/ <- Commands at root of install_folder + # cypress/ <- Framework-specific subdirectory + # support/ + # e2e/ + + expect(File).to exist(File.join(destination_root, 'e2e', 'cypress.config.js')) + expect(File).to exist(File.join(destination_root, 'e2e', 'e2e_helper.rb')) + expect(File).to be_directory(File.join(destination_root, 'e2e', 'app_commands')) + expect(File).to be_directory(File.join(destination_root, 'e2e', 'cypress')) + expect(File).to be_directory(File.join(destination_root, 'e2e', 'cypress', 'support')) + expect(File).to be_directory(File.join(destination_root, 'e2e', 'cypress', 'e2e')) + end + end + + def run_generator(args, options) + generator_options = [] + options.each do |key, value| + generator_options << "--#{key}=#{value}" + end + + CypressOnRails::InstallGenerator.start( + args + generator_options, + { + destination_root: destination_root, + shell: Thor::Shell::Basic.new, + behavior: :invoke + } + ) + end +end