diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f40ae6b..10e396d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: steps: # Downloads a copy of the code in your repository before running CI tests - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v6 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of sonarcloud analysis @@ -26,18 +26,12 @@ jobs: run: | bundle exec rubocop --format progress --format json --out rubocop-result.json - # This includes an extra run step. The sonarcloud analysis will be run in a docker container with the current - # folder mounted as `/github/workspace`. The problem is when the .resultset.json file is generated it will - # reference the code in the current folder. So to enable sonarcloud to matchup code coverage with the files we use - # sed to update the references in .resultset.json - # https://community.sonarsource.com/t/code-coverage-doesnt-work-with-github-action/16747/6 - name: Run unit tests run: | bundle exec rspec - sed -i 's/\/home\/runner\/work\/quke\/quke\//\/github\/workspace\//g' coverage/.resultset.json - name: Analyze with SonarCloud - uses: sonarsource/sonarcloud-github-action@master + uses: sonarsource/sonarqube-scan-action@v7 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This is provided automatically by GitHub - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # This needs to be set in your repo; settings -> secrets + SONAR_TOKEN: ${{ secrets.RUBY_SONAR_TOKEN }} # This needs to be set in your repo; settings -> secrets diff --git a/.rubocop.yml b/.rubocop.yml index 31e3ba5..4943e15 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,6 +4,9 @@ inherit_gem: defra_ruby_style: - default.yml -require: +plugins: - rubocop-rake - rubocop-rspec + +AllCops: + TargetRubyVersion: 3.4 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6ff8538..f200707 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,31 +1,22 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2023-08-14 14:02:05 UTC using RuboCop version 1.56.0. +# on 2026-05-01 16:22:43 UTC using RuboCop version 1.86.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 8 -# Configuration parameters: EnforcedStyle, AllowedGems, Include. -# SupportedStyles: Gemfile, gems.rb, gemspec -# Include: **/*.gemspec, **/Gemfile, **/gems.rb -Gemspec/DevelopmentDependencies: - Exclude: - - 'quke.gemspec' - # Offense count: 1 -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec -Gemspec/RequiredRubyVersion: +# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch. +Lint/DuplicateBranch: Exclude: - - 'quke.gemspec' + - 'lib/quke/driver_registration.rb' # Offense count: 1 -# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. -Lint/DuplicateBranch: +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. +Metrics/AbcSize: Exclude: - - 'lib/quke/driver_registration.rb' + - 'lib/quke/driver_configuration.rb' # Offense count: 16 # Configuration parameters: Prefixes, AllowedPatterns. @@ -36,7 +27,7 @@ RSpec/ContextWording: - 'spec/quke/driver_configuration_spec.rb' - 'spec/quke/driver_registration_spec.rb' -# Offense count: 18 +# Offense count: 17 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 22 @@ -57,7 +48,7 @@ RSpec/IdenticalEqualityAssertion: RSpec/MessageSpies: EnforcedStyle: receive -# Offense count: 10 +# Offense count: 9 RSpec/MultipleExpectations: Max: 17 @@ -75,8 +66,3 @@ RSpec/NamedSubject: - 'spec/quke/browserstack_status_reporter_spec.rb' - 'spec/quke/configuration_spec.rb' - 'spec/quke/proxy_configuration_spec.rb' - -# Offense count: 1 -# Configuration parameters: AllowedGroups. -RSpec/NestedGroups: - Max: 4 diff --git a/.ruby-version b/.ruby-version index be94e6f..1cf8253 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.4.6 diff --git a/Gemfile b/Gemfile index 1a7315c..fd1497d 100644 --- a/Gemfile +++ b/Gemfile @@ -13,9 +13,10 @@ group :development, :test do gem "rdoc" gem "rspec" gem "rubocop" + gem "rubocop-factory_bot" gem "rubocop-rake" gem "rubocop-rspec" - gem "simplecov", "~> 0.17.1" + gem "simplecov", "~> 0.22" gem "simplecov-json", require: false gem "webmock" end diff --git a/lib/features/support/env.rb b/lib/features/support/env.rb index d55261b..3b04d48 100644 --- a/lib/features/support/env.rb +++ b/lib/features/support/env.rb @@ -15,15 +15,25 @@ driver_reg = Quke::DriverRegistration.new(driver_config, Quke::Quke.config) driver = driver_reg.register(Quke::Quke.config.driver) -# We need bs_local to be declared outside of the AfterConfiguration block below -# so that it's available in the at_exit block. -# Did try simply calling it @bs_local inside the AfterConfiguration but that -# just kept causing Quke to crash immediately (shrug!) bs_local = nil Capybara.default_driver = driver Capybara.javascript_driver = driver +# Chrome 147+ raises UnknownError with "Node with given id does not belong to +# the document" instead of StaleElementReferenceError when a cached DOM node +# reference becomes invalid after navigation. Adding UnknownError to Capybara's +# retriable errors makes Chrome behave like Firefox, which retries internally. +if Quke::Quke.config.driver == "chrome" + module QukeChromeStaleNodeFix + def invalid_element_errors + @invalid_element_errors ||= + super + [::Selenium::WebDriver::Error::UnknownError] + end + end + Capybara::Selenium::Driver.prepend(QukeChromeStaleNodeFix) +end + # default_max_wait_time is the maximum time Capybara will wait for an element # to appear. You may wish to override it if you are having to deal with a slow # or unresponsive web site. @@ -41,27 +51,21 @@ # which can mess up your project structure. Capybara.save_path = "tmp/" -# There aren't specific hooks we can attach to that only get called once before -# and after all tests have run in Cucumber. Therefore the next best thing is to -# hook into the AfterConfiguration and at_exit blocks. -# -# As its name suggests, this gets called after Cucumber has been configured i.e. -# all the steps above are complete. Fortunately this is before the tests start -# running so its the best place for us to start up the browserstack local -# testing binary (if it's required) -AfterConfiguration do +# BeforeAll / AfterAll run exactly once around the entire test run (Cucumber 8+). +# We use BeforeAll to start the BrowserStack Local binary when local testing is +# enabled, and AfterAll to stop it cleanly inside Cucumber's lifecycle. +BeforeAll do if Quke::Quke.config.browserstack.test_locally? bs_local = BrowserStack::Local.new - - # starts the Local instance with the required arguments via its management - # API bs_local.start(Quke::Quke.config.browserstack.local_testing_args) end end -# This is the very last thing Cucumber calls that we can hook onto. Typically -# used for final cleanup, we make use of it to kill our browserstack local -# testing binary, and update the status of the session in browserstack +AfterAll do + bs_local&.stop +end + +# Update the BrowserStack session status (pass/fail) after all tests complete. at_exit do # Because of the way cucumber works everthing is made global. This also means # any variables we set also need to be made global so they can be accessed @@ -80,8 +84,4 @@ end end # rubocop:enable Style/GlobalVars - if bs_local && Quke::Quke.config.browserstack.test_locally? - # stop the local instance - bs_local.stop - end end diff --git a/lib/quke/configuration.rb b/lib/quke/configuration.rb index 1f84144..2afbee6 100644 --- a/lib/quke/configuration.rb +++ b/lib/quke/configuration.rb @@ -110,7 +110,7 @@ def pause def stop_on_error # This use of Yaml.load to convert a string to a boolean comes from # http://stackoverflow.com/a/21804027/6117745 - YAML.load(@data["stop_on_error"]) + YAML.safe_load(@data["stop_on_error"]) end # Returns the value set for +display_failures+. @@ -267,7 +267,7 @@ def default_data!(data) # rubocop:disable Style/InverseMethods "display_failures" => !(data["display_failures"].to_s.downcase.strip == "false"), # rubocop:enable Style/InverseMethods - "custom" => (data["custom"] || nil) + "custom" => data["custom"] || nil ) end # rubocop:enable Metrics/AbcSize @@ -278,7 +278,7 @@ def load_yml_data if File.exist? self.class.file_location # YAML.load_file returns false if the file exists but is empty. So # added the || {} to ensure we always return a hash from this method - YAML.load_file(self.class.file_location) || {} + YAML.safe_load_file(self.class.file_location) || {} else {} end diff --git a/lib/quke/driver_configuration.rb b/lib/quke/driver_configuration.rb index bd1e429..05c0d67 100644 --- a/lib/quke/driver_configuration.rb +++ b/lib/quke/driver_configuration.rb @@ -58,6 +58,9 @@ def chrome options = Selenium::WebDriver::Options.chrome(args: args) + options.add_argument("--window-size=1920,1080") + options.add_argument("--disable-back-forward-cache") + options.add_argument("--disable-features=BackForwardCache,BuiltInJsonViewer") options.add_argument("--proxy-server=#{config.proxy.host}:#{config.proxy.port}") if config.proxy.use_proxy? options.add_argument("--proxy-bypass-list=#{no_proxy}") unless config.proxy.no_proxy.empty? @@ -97,7 +100,7 @@ def chrome # def firefox options = Selenium::WebDriver::Firefox::Options.new(profile: firefox_profile) - options.headless! if config.headless + options.add_argument("--headless") if config.headless options end diff --git a/lib/quke/driver_registration.rb b/lib/quke/driver_registration.rb index 9c75424..78292dc 100644 --- a/lib/quke/driver_registration.rb +++ b/lib/quke/driver_registration.rb @@ -97,7 +97,7 @@ def browserstack app, browser: :remote, url: @config.browserstack.url, - desired_capabilities: @driver_config.browserstack + capabilities: [@driver_config.browserstack] ) # :simplecov_ignore: end diff --git a/lib/quke/version.rb b/lib/quke/version.rb index c71e5d0..b77fb4c 100644 --- a/lib/quke/version.rb +++ b/lib/quke/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Quke # :nodoc: - VERSION = "0.10.0" + VERSION = "0.11.0" end diff --git a/quke.gemspec b/quke.gemspec index ab87556..74234d3 100644 --- a/quke.gemspec +++ b/quke.gemspec @@ -35,25 +35,25 @@ Gem::Specification.new do |spec| "public gem pushes." end - spec.required_ruby_version = ">= 2.4" + spec.required_ruby_version = ">= 3.4" # We need the cucumber gem to use cucumber, obviously! - spec.add_dependency "cucumber", "~> 3.1" + spec.add_dependency "cucumber", "~> 11.0" # We use capybara to drive whichever browser we are using, and by drive we # mean things like fill_in x, click_on y etc. Capybara makes it much easier to # do this, though if you're willing to go a level lower you can write your own # code to tell selenium how to interact with a web page - spec.add_dependency "capybara", "~> 3.14" + spec.add_dependency "capybara", "~> 3.40" # We bring in rspec-expectations to simplify how to actually test if a page is # correct. For example you can test you are on the right page in a step using # expect(page).to have_text 'Welcome to test nirvana!' - spec.add_dependency "rspec-expectations", "~> 3.8" + spec.add_dependency "rspec-expectations", "~> 3.13" # selenium-webdriver is used to drive browsers like Firefox, Chrome and # Internet Explorer. - spec.add_dependency "selenium-webdriver", "~> 4.1" + spec.add_dependency "selenium-webdriver", "~> 4.43" # Experience has shown that keeping tests dry helps make them more # maintainable over time. One practice that helps is the use of the @@ -63,13 +63,13 @@ Gem::Specification.new do |spec| # different steps. Site_Prism provides a page object framework, and we build # it into the gem so users of Quke don't have to add and setup this dependency # themselves - spec.add_dependency "site_prism", "~> 3.0" + spec.add_dependency "site_prism", "~> 6.0" # Capybara includes a method called save_and_open_page. Without Launchy it # will still save to file a copy of the source html of the page in question # at that time. However simply adding this line into the gemfile means it # will instead open in the default browser instead. - spec.add_dependency "launchy", "~> 2.4" + spec.add_dependency "launchy", "~> 3.1" # Ruby bindings for BrowserStack Local. This gem handles downloading and # installing the right version of the binary for the OS Quke is running on, diff --git a/spec/quke/driver_configuration_spec.rb b/spec/quke/driver_configuration_spec.rb index e6f3558..df7edf0 100644 --- a/spec/quke/driver_configuration_spec.rb +++ b/spec/quke/driver_configuration_spec.rb @@ -10,7 +10,7 @@ it "returns an instance of Chrome::Options where the proxy details are NOT set" do Quke::Configuration.file_location = data_path(".no_file.yml") config = Quke::Configuration.new - expect(described_class.new(config).chrome.args).to eq([]) + expect(described_class.new(config).chrome.args).not_to include(start_with("--proxy-server")) end end @@ -18,10 +18,8 @@ it "returns an instance of Chrome::Options containing basic proxy settings" do Quke::Configuration.file_location = data_path(".proxy_basic.yml") config = Quke::Configuration.new - expect(described_class.new(config).chrome.args).to eq( - [ - "--proxy-server=#{config.proxy.host}:#{config.proxy.port}" - ] + expect(described_class.new(config).chrome.args).to include( + "--proxy-server=#{config.proxy.host}:#{config.proxy.port}" ) end end @@ -30,11 +28,9 @@ it "returns an instance of Chrome::Options containing proxy settings including no-proxy details" do Quke::Configuration.file_location = data_path(".proxy.yml") config = Quke::Configuration.new - expect(described_class.new(config).chrome.args).to eq( - [ - "--proxy-server=#{config.proxy.host}:#{config.proxy.port}", - "--proxy-bypass-list=127.0.0.1;192.168.0.1" - ] + expect(described_class.new(config).chrome.args).to include( + "--proxy-server=#{config.proxy.host}:#{config.proxy.port}", + "--proxy-bypass-list=127.0.0.1;192.168.0.1" ) end end @@ -43,11 +39,7 @@ it "returns an instance of Chrome::Options containing the specified user-agent" do Quke::Configuration.file_location = data_path(".user_agent.yml") config = Quke::Configuration.new - expect(described_class.new(config).chrome.args).to eq( - [ - "--user-agent=#{config.user_agent}" - ] - ) + expect(described_class.new(config).chrome.args).to include("--user-agent=#{config.user_agent}") end end @@ -55,7 +47,7 @@ it "returns an instance of Chrome::Options set to run the browser in headless mode" do Quke::Configuration.file_location = data_path(".headless.yml") config = Quke::Configuration.new - expect(described_class.new(config).chrome.args).to eq(["--headless=new"]) + expect(described_class.new(config).chrome.args).to include("--headless=new") end end @@ -129,7 +121,7 @@ it "returns an instance of Firefox::Options set to run the browser in headless mode" do Quke::Configuration.file_location = data_path(".headless.yml") config = Quke::Configuration.new - expect(described_class.new(config).chrome.args).to eq(["--headless=new"]) + expect(described_class.new(config).firefox.args).to include("--headless") end end