diff --git a/.rubocop.yml b/.rubocop.yml index cb30d466..08b3beb8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,5 @@ require: + - rubocop-capybara - rubocop-factory_bot - rubocop-performance - rubocop-rails diff --git a/Gemfile b/Gemfile index dd24e039..3d64eb6c 100644 --- a/Gemfile +++ b/Gemfile @@ -65,11 +65,12 @@ group :development, :test do gem 'bootsnap', '>= 1.16.0', require: false # Call 'byebug' in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + gem 'capybara' + gem 'cuprite' gem 'dotenv-rails' gem 'pry-byebug', '~> 3.9' gem 'rails-controller-testing' gem 'rspec-rails', '~> 6.0.3' - gem 'selenium-webdriver' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 0f223cb1..4d364809 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,6 +126,15 @@ GEM capistrano-rails (1.6.3) capistrano (~> 3.1) capistrano-bundler (>= 1.1, < 3) + capybara (3.39.2) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) carrierwave (3.0.3) activemodel (>= 6.0.0) activesupport (>= 6.0.0) @@ -142,6 +151,9 @@ GEM colorize (0.8.1) concurrent-ruby (1.2.2) crass (1.0.6) + cuprite (0.14.3) + capybara (~> 3.0) + ferrum (~> 0.13.0) database_cleaner (2.0.2) database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (2.1.0) @@ -191,6 +203,11 @@ GEM fasterer (0.10.1) colorize (~> 0.7) ruby_parser (>= 3.19.1) + ferrum (0.13) + addressable (~> 2.5) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (>= 0.6, < 0.8) ffi (1.16.3) flay (2.13.1) erubi (~> 1.10) @@ -264,6 +281,7 @@ GEM actionmailer (>= 5.2) activemodel (>= 5.2) marcel (1.0.2) + matrix (0.4.2) memory_profiler (1.0.1) meta-tags (2.18.0) actionpack (>= 3.2.0, < 7.1) @@ -441,7 +459,6 @@ GEM simplecov (>= 0.22.0) tty-which (~> 0.5.0) virtus (~> 2.0) - rubyzip (2.3.2) sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -459,10 +476,6 @@ GEM tilt scss_lint (0.60.0) sass (~> 3.5, >= 3.5.5) - selenium-webdriver (4.13.1) - rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) - websocket (~> 1.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) @@ -521,10 +534,12 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - websocket (1.2.10) + webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) zeitwerk (2.6.12) PLATFORMS @@ -542,8 +557,10 @@ DEPENDENCIES bullet byebug capistrano-rails (~> 1.6) + capybara carrierwave (~> 3.0) colored + cuprite database_cleaner (~> 2.0.1) derailed_benchmarks dockerfile-rails (>= 1.5) @@ -585,7 +602,6 @@ DEPENDENCIES rubycritic sass-rails (~> 6.0) scss_lint - selenium-webdriver shoulda-matchers simple_form (~> 5.2) slim-rails (~> 3.6) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6909c48d..672210fb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,6 @@ RSpec.configure do |config| config.filter_run :focus config.order = :random + config.profile_examples = true config.run_all_when_everything_filtered = true end diff --git a/spec/support/system/capybara_setup.rb b/spec/support/system/capybara_setup.rb new file mode 100644 index 00000000..1337cf3f --- /dev/null +++ b/spec/support/system/capybara_setup.rb @@ -0,0 +1,62 @@ +# Make server listening on all hosts +Capybara.server_host = '0.0.0.0' +Capybara.server_port = 3001 + +# Use a hostname accessible from the outside world +Capybara.app_host = "http://#{ENV.fetch('APP_HOST') { `hostname`.strip&.downcase || '0.0.0.0' }}" + +# Which domain to use when setting cookies directly in tests. +CAPYBARA_COOKIE_DOMAIN = URI.parse(Capybara.app_host).host.then do |host| + # If host is a top-level domain + next host unless host.include?('.') + + ".#{host}" +end + +# Usually, especially when using Selenium, developers tend to increase the max wait time. +# With Cuprite, there is no need for that. +# We use a Capybara default value here explicitly. +Capybara.default_max_wait_time = 2 + +# Normalize whitespaces when using `has_text?` and similar matchers, +# i.e., ignore newlines, trailing spaces, etc. +# That makes tests less dependent on slightly UI changes. +Capybara.default_normalize_ws = true + +# Where to store system tests artifacts (e.g. screenshots, downloaded files, etc.). +# It could be useful to be able to configure this path from the outside (e.g., on CI). +# Capybara.save_path = ENV.fetch("CAPYBARA_ARTIFACTS", "./tmp/capybara") + +Capybara.singleton_class.prepend( + Module.new do + attr_accessor :last_used_session + + def using_session(name, &) + self.last_used_session = name + super + ensure + self.last_used_session = nil + end + end +) + +RSpec.configure do |config| + config.include ActionView::RecordIdentifier, type: :system + + # Make urls in mailers contain the correct server host + config.around(:each, type: :system) do |ex| + was_host = Rails.application.default_url_options[:host] + Rails.application.default_url_options[:host] = Capybara.server_host + ex.run + Rails.application.default_url_options[:host] = was_host + end + + config.before(:each, type: :system) do + driven_by :rack_test + end + + config.before(:each, :js, type: :system) do + # Use JS driver always + driven_by Capybara.javascript_driver + end +end diff --git a/spec/support/system/cuprite_setup.rb b/spec/support/system/cuprite_setup.rb new file mode 100644 index 00000000..5bbe3bc3 --- /dev/null +++ b/spec/support/system/cuprite_setup.rb @@ -0,0 +1,72 @@ +# Cuprite is a modern Capybara driver which uses Chrome CDP API +# instead of Selenium & co. + +remote_chrome_url = ENV.fetch('CHROME_URL') { 'http://0.0.0.0:3000' } + +# Chrome doesn't allow connecting to CDP by hostnames (other than localhost), +# but allows using IP addresses. +if remote_chrome_url&.match?(/host.docker.internal/) + require 'resolv' + + uri = URI.parse(remote_chrome_url) + ip = Resolv.getaddress(uri.host) + + remote_chrome_url = remote_chrome_url.sub('host.docker.internal', ip) +end + +REMOTE_CHROME_URL = remote_chrome_url +REMOTE_CHROME_HOST, REMOTE_CHROME_PORT = + if REMOTE_CHROME_URL + URI.parse(REMOTE_CHROME_URL).then do |remote_uri| + [remote_uri.host, remote_uri.port] + end + end + +# Check whether the remote chrome is running and configure the Capybara +# driver for it. +remote_chrome = + begin + if REMOTE_CHROME_URL.nil? + false + else + Socket.tcp(REMOTE_CHROME_HOST, REMOTE_CHROME_PORT, connect_timeout: 1).close + true + end + rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError + false + end + +remote_options = remote_chrome ? { url: REMOTE_CHROME_URL } : {} + +require 'capybara/cuprite' + +Capybara.register_driver(:cuprite) do |app| + Capybara::Cuprite::Driver.new( + app, + **{ + window_size: [1200, 800], + browser_options: remote_chrome ? { 'no-sandbox' => nil } : {}, + inspector: true + }.merge(remote_options) + ) +end + +Capybara.default_driver = Capybara.javascript_driver = :cuprite + +# Add shortcuts for cuprite-specific debugging helpers +module CupriteHelpers + def pause + page.driver.pause + end + + def debug(binding = nil) + $stdout.puts 'šŸ”Ž Open Chrome inspector at http://localhost:3333' + return binding.break if binding + + page.driver.pause + end +end + +RSpec.configure do |config| + config.include CupriteHelpers, type: :system +end diff --git a/spec/support/system/precompile_assets.rb b/spec/support/system/precompile_assets.rb new file mode 100644 index 00000000..9dbbbeba --- /dev/null +++ b/spec/support/system/precompile_assets.rb @@ -0,0 +1,29 @@ +# Precompile assets before running tests to avoid timeouts. +# Do not precompile if webpack-dev-server is running +# (NOTE: MUST be launched with RAILS_ENV=test) +RSpec.configure do |config| + config.before(:suite) do + examples = RSpec.world.filtered_examples.values.flatten + has_no_system_tests = examples.none? { |example| example.metadata[:type] == :system } + + if has_no_system_tests + $stdout.puts "\nšŸš€ļøļø No system test selected. Skip assets compilation.\n" + next + end + + $stdout.puts "\nšŸ¢ Precompiling assets.\n" + original_stdout = $stdout.clone + + start = Time.current + begin + $stdout.reopen(File.new('/dev/null', 'w')) + + require 'rake' + Rails.application.load_tasks + Rake::Task['assets:precompile'].invoke('silent') + ensure + $stdout.reopen(original_stdout) + $stdout.puts "Finished in #{(Time.current - start).round(2)} seconds" + end + end +end diff --git a/spec/system/check_version_of_javascript_libraries_spec.rb b/spec/system/check_version_of_javascript_libraries_spec.rb new file mode 100644 index 00000000..36343e46 --- /dev/null +++ b/spec/system/check_version_of_javascript_libraries_spec.rb @@ -0,0 +1,18 @@ +require 'system_helper' + +RSpec.describe 'Application loads JavaScript libraries' do + it 'exist and allow us to check their versions', :js do + visit root_url + + browser = page.driver.browser + bootstrap_version = browser.evaluate('bootstrap.Tooltip.VERSION') + jquery_version = browser.evaluate('jQuery().jquery') + leaflet_version = browser.evaluate('L.version') + + aggregate_failures('verify version of JavaScript libraries') do + expect(bootstrap_version).to eq('5.3.1') + expect(jquery_version).to eq('3.7.1') + expect(leaflet_version).to eq('1.9.4') + end + end +end diff --git a/spec/system/seeds_loading_spec.rb b/spec/system/seeds_loading_spec.rb new file mode 100644 index 00000000..4b9049b2 --- /dev/null +++ b/spec/system/seeds_loading_spec.rb @@ -0,0 +1,16 @@ +require 'system_helper' + +RSpec.describe 'Application loads the seeds' do + it 'is done correctly and homepage displays team member profiles' do + Rails.application.load_seed + + visit '/' + + aggregate_failures('verify first names of all team members') do + expect(page).to have_content 'Agnieszka' + expect(page).to have_content 'Alex' + expect(page).to have_content 'Anna' + expect(page).to have_content 'Grzegorz' + end + end +end diff --git a/spec/system_helper.rb b/spec/system_helper.rb new file mode 100644 index 00000000..db50469f --- /dev/null +++ b/spec/system_helper.rb @@ -0,0 +1,3 @@ +require 'rails_helper' + +Dir[Rails.root.join('spec/system/support/**/*.rb')].each { |f| require f }