From 0700f7ed546dc69e09e2acd78d318fb0bd325284 Mon Sep 17 00:00:00 2001 From: Alex Malaszkiewicz Date: Tue, 3 Oct 2023 10:24:46 +0200 Subject: [PATCH 1/6] Set the profile examples value in RSpec > Defaults profile_examples to 10 examples [1] > when @profile_examples is true. This value determines the number of the slowest tests displayed. [1]: https://rspec.info/documentation/3.1/rspec-core/RSpec/Core/Configuration.html#profile_examples-instance_method --- spec/spec_helper.rb | 1 + 1 file changed, 1 insertion(+) 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 From fbd26ca2840f40c315e0c6b143561e418c113e98 Mon Sep 17 00:00:00 2001 From: Alex Malaszkiewicz Date: Tue, 3 Oct 2023 16:49:56 +0200 Subject: [PATCH 2/6] Add Capybara & other useful gems to Gemfile We want to create high-level tests in Selenium [1] [2] using Capybara. [3] To make using Selenium easier, it is worth installing webdrivers. [4] > Run Selenium tests more easily with automatic installation > and updates for all supported webdrivers. However, recently there have been major changes, which we can read about in the webdrivers documentation. > With Google's new Chrome for Testing project, [5] > and Selenium's new Selenium Manager feature, > what is required of this gem has changed. > > If you can update to the latest version of Selenium (4.11+), > please do so and stop requiring this gem. > Provide feedback or raise issues to Selenium Project My further research led me to an article published by Evil Martians. [6] This in turn led me to Cuprite [7] [8] and related gems like Vessel [9] and Ferrum. [10] > Cuprite is a pure Ruby driver > (read as no Selenium/WebDriver/ChromeDriver dependency) > for Capybara. > Vessel - high-level web crawling framework > It is Ruby high-level web crawling framework > based on Ferrum for extracting the data you need from websites. > Ferrum - high-level API to control Chrome in Ruby > It is Ruby clean and high-level API to Chrome. > Runs headless by default, > but you can configure it to run in a headful mode. > Ferrum connects to the browser by CDP protocol > and there's no Selenium/WebDriver/ChromeDriver dependency. [1]: https://www.selenium.dev/ [2]: https://github.com/SeleniumHQ/selenium [3]: https://github.com/teamcapybara/capybara [4]: https://github.com/titusfortner/webdrivers [5]: https://developer.chrome.com/blog/chrome-for-testing/ [6]: https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing [7]: https://github.com/rubycdp/cuprite [8]: https://cuprite.rubycdp.com/ [9]: https://github.com/rubycdp/vessel [10]: https://github.com/rubycdp/ferrum --- Gemfile | 3 ++- Gemfile.lock | 30 +++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) 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) From e03283152d21f4c249ae7b98b91fef257060ed54 Mon Sep 17 00:00:00 2001 From: Alex Malaszkiewicz Date: Thu, 5 Oct 2023 11:04:10 +0200 Subject: [PATCH 3/6] Set up Capybara & Cuprite Based on the article I found earlier, [1] I prepared a configuration for Capybara and Cuprite. The `system_helper.rb` file contain general RSpec configuration for system tests. The `capybara_setup.rb` contains configuration for Capybara framework. The `cuprite_setup.rb` is responsible for configuring Cuprite. The `precompile_assets.rb` file is responsible for precompiling assets before running system tests. > Why precompile assets manually if Rails can do that automatically? > The problem is that Rails precompiles assets lazily > (i.e., the first time you request an asset), > and this could make the first test example run much slower > and even encounter random timeout exceptions. Generally, I didn't want to go exactly in the direction presented by Evil Martians. My goal was to simplify their setup. [1]: https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing --- spec/support/system/capybara_setup.rb | 62 ++++++++++++++++++++ spec/support/system/cuprite_setup.rb | 72 ++++++++++++++++++++++++ spec/support/system/precompile_assets.rb | 29 ++++++++++ spec/system_helper.rb | 3 + 4 files changed, 166 insertions(+) create mode 100644 spec/support/system/capybara_setup.rb create mode 100644 spec/support/system/cuprite_setup.rb create mode 100644 spec/support/system/precompile_assets.rb create mode 100644 spec/system_helper.rb 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_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 } From 50e1290843f3ae77fcd0ab6f3aaa936f3a44c758 Mon Sep 17 00:00:00 2001 From: Alex Malaszkiewicz Date: Wed, 4 Oct 2023 09:34:38 +0200 Subject: [PATCH 4/6] Create test to verify seeds Recently, an error occurred that our seeds were not compatible with the database structure. We want to avoid this in the future, so I created a simple integration test. --- spec/system/seeds_loading_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 spec/system/seeds_loading_spec.rb 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 From 32911ed4c0ffaf2ae0788212bebe24b6366d5df0 Mon Sep 17 00:00:00 2001 From: Alex Malaszkiewicz Date: Thu, 5 Oct 2023 11:34:25 +0200 Subject: [PATCH 5/6] Create a test which verify JavaScript libraries We want our test to monitor what versions of the JavaScript libraries we are using. --- ...eck_version_of_javascript_libraries_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 spec/system/check_version_of_javascript_libraries_spec.rb 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 From f04ff58a5fc45831110928d2baaf3d87d382ce70 Mon Sep 17 00:00:00 2001 From: Alex Malaszkiewicz Date: Thu, 5 Oct 2023 11:42:39 +0200 Subject: [PATCH 6/6] Enable Rubocop Capybara extension After adding the tests in Capybara, our Rubocop informed us: > The following RuboCop extension libraries are installed > but not loaded in config: > * rubocop-capybara So I added this extension to our Rubocop configuration. --- .rubocop.yml | 1 + 1 file changed, 1 insertion(+) 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