diff --git a/.github/workflows/test_solidus.yml b/.github/workflows/test_solidus.yml index a1818a5b63e..8471354814d 100644 --- a/.github/workflows/test_solidus.yml +++ b/.github/workflows/test_solidus.yml @@ -16,7 +16,10 @@ on: jobs: RSpec: name: Rails ${{ matrix.rails }}, Ruby ${{ matrix.ruby }}, ${{ matrix.database }}, ${{ matrix.storage }} - runs-on: ubuntu-22.04 + # NOTE: ubuntu-24.04 is required for libvips 8.13+. ActiveStorage 8.1 refuses to + # load with older libvips, because it cannot disable the unfuzzed operations that + # are unsafe on untrusted content. ubuntu-22.04 only ships libvips 8.12.1. + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: @@ -83,11 +86,21 @@ jobs: with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - - uses: awalsh128/cache-apt-pkgs-action@v1 + # NOTE: Install libvips with plain apt-get rather than a caching action. A + # cached install can report a cache hit while restoring zero packages, which + # leaves libvips missing and surfaces much later as an opaque + # "undefined method 'new' for nil" from ImageProcessing::Vips inside the + # specs. `vips --version` makes a bad install fail here instead. + # + # ruby-vips binds libvips.so through FFI at runtime, so the docs and the GUI + # tools apt recommends are dead weight; libvips-tools is requested explicitly + # because it provides the `vips` binary used by the check below. + - name: Install libvips if: ${{ matrix.storage == 'activestorage' }} - name: Install libvips - with: - packages: libvips-dev + run: | + sudo apt-get update + sudo apt-get install -yq --no-install-recommends libvips-dev libvips-tools + vips --version - name: Setup coverage id: setup-coverage if: ${{ inputs.coverage && matrix.rails == '8.0' && matrix.ruby == '3.4' }} diff --git a/admin/spec/spec_helper.rb b/admin/spec/spec_helper.rb index d69fa4e57b6..0bd65d5102e 100644 --- a/admin/spec/spec_helper.rb +++ b/admin/spec/spec_helper.rb @@ -53,6 +53,7 @@ Capybara.disable_animation = true Capybara.default_max_wait_time = ENV["DEFAULT_MAX_WAIT_TIME"].to_f if ENV["DEFAULT_MAX_WAIT_TIME"].present? Capybara.enable_aria_label = true +Capybara.server = :puma, {Silent: true} # A workaround for https://github.com/rspec/rspec-rails/issues/1897 # DATABASE CLEANER require "database_cleaner" diff --git a/core/lib/spree/testing_support/capybara_ext.rb b/core/lib/spree/testing_support/capybara_ext.rb index a567e23c819..0f3849f6796 100644 --- a/core/lib/spree/testing_support/capybara_ext.rb +++ b/core/lib/spree/testing_support/capybara_ext.rb @@ -4,7 +4,15 @@ module Spree module TestingSupport module CapybaraExt def click_icon(type) - find(".fa-#{type}").click + el = find(".fa-#{type}", visible: :all) + begin + el.click + rescue Selenium::WebDriver::Error::ElementClickInterceptedError + # When a floating element (eg. tooltips/overlays) intercepts the click, + # scroll the target into view and dispatch a click via JS to keep tests stable. + page.execute_script('arguments[0].scrollIntoView({block: "center"});', el.native) + page.execute_script("arguments[0].click();", el.native) + end end def eventually_fill_in(field, options = {}) diff --git a/core/lib/spree/testing_support/dummy_app.rb b/core/lib/spree/testing_support/dummy_app.rb index e2be2ae170f..2489e2db051 100644 --- a/core/lib/spree/testing_support/dummy_app.rb +++ b/core/lib/spree/testing_support/dummy_app.rb @@ -3,6 +3,18 @@ ENV["RAILS_ENV"] = "test" ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] = "1" +# Speed up the sqlite runs: `fast_sqlite` patches SQLite3::Database#initialize to +# set `PRAGMA synchronous = OFF` and `journal_mode = MEMORY`, trading durability we +# don't need for a database the suite recreates anyway. +# +# NOTE: The gem is declared `require: false`, and only when DB is sqlite, so it is +# genuinely absent for the mysql and postgres runs. +begin + require "fast_sqlite" +rescue LoadError + # Not running against sqlite, nothing to speed up. +end + require "rails" require "active_record/railtie" require "action_controller/railtie" @@ -106,7 +118,10 @@ class Application < ::Rails::Application # Set the preview path within the dummy app: if ActionMailer::Base.respond_to? :preview_paths # Rails 7.1+ - config.action_mailer.preview_paths << File.expand_path("dummy_app/mailer_previews", __dir__) + # Some Rails versions return a frozen array here; assign a new array + # to avoid FrozenError when augmenting the paths. + existing = Array(config.action_mailer.preview_paths) + config.action_mailer.preview_paths = existing + [File.expand_path("dummy_app/mailer_previews", __dir__)] else config.action_mailer.preview_path = File.expand_path("dummy_app/mailer_previews", __dir__) end diff --git a/core/spec/lib/spree/testing_support/capybara_ext_spec.rb b/core/spec/lib/spree/testing_support/capybara_ext_spec.rb new file mode 100644 index 00000000000..1d0390a69bf --- /dev/null +++ b/core/spec/lib/spree/testing_support/capybara_ext_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require "rails_helper" +require "selenium-webdriver" +require "spree/testing_support/capybara_ext" + +RSpec.describe Spree::TestingSupport::CapybaraExt do + subject(:page_object) do + Class.new do + include Spree::TestingSupport::CapybaraExt + + attr_reader :page + + def initialize(element:, page:) + @element = element + @page = page + end + + def find(*) = @element + end.new(element: element, page: page) + end + + let(:element) { instance_double(Capybara::Node::Element, native: :native_element) } + let(:page) { instance_double(Capybara::Session, execute_script: nil) } + + describe "#click_icon" do + it "clicks the icon it finds" do + allow(element).to receive(:click) + + page_object.click_icon(:edit) + + expect(element).to have_received(:click) + end + + context "when a floating element intercepts the click" do + before do + allow(element).to receive(:click) + .and_raise(Selenium::WebDriver::Error::ElementClickInterceptedError) + end + + it "scrolls the element into view and clicks it via JavaScript" do + page_object.click_icon(:edit) + + expect(page).to have_received(:execute_script) + .with('arguments[0].scrollIntoView({block: "center"});', :native_element).ordered + expect(page).to have_received(:execute_script) + .with("arguments[0].click();", :native_element).ordered + end + end + + context "when finding the icon is itself intercepted" do + subject(:page_object) do + Class.new do + include Spree::TestingSupport::CapybaraExt + + attr_reader :page + + def initialize(page:) + @page = page + end + + def find(*) + raise Selenium::WebDriver::Error::ElementClickInterceptedError + end + end.new(page: page) + end + + # The fallback needs an element to scroll to, so a failure to find one has to + # surface as itself rather than as a NoMethodError on nil. + it "lets the error through instead of retrying without an element" do + expect { page_object.click_icon(:edit) } + .to raise_error(Selenium::WebDriver::Error::ElementClickInterceptedError) + end + end + end +end diff --git a/docker-compose.yml b/docker-compose.yml index 7247adc0933..cf2b44666d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: "3.7" services: mysql: @@ -18,12 +18,12 @@ services: - postgres:/var/lib/postgresql/data:cached app: - shm_size: '512mb' + shm_size: "512mb" build: context: .dockerdev dockerfile: Dockerfile args: - RUBY_VERSION: "3.1" + RUBY_VERSION: "3.4.6" PG_VERSION: 13 NODE_VERSION: 20 MYSQL_VERSION: "8.0"