Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.github/
.ruby-lsp/
coverage/
tmp/

# Ignore report files
*.attempt_*.png
*.diff.png
*.base.png
*.attempt_*.webp
*.diff.webp
*.base.webp
3 changes: 3 additions & 0 deletions .github/actions/setup-ruby-and-dependencies/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ runs:
# fallback if cache version is outdated
- run: sudo apt-get -qq install libvips
shell: bash

- run: sudo sed -i 's/true/false/g' /etc/fonts/conf.d/10-yes-antialias.conf
shell: bash
12 changes: 7 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ env:
JAVA_OPTS: -Xmn2g -Xms6g -Xmx6g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -Xss1m
-XX:+UseG1GC -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:ReservedCodeCacheSize=256m
-XX:+UseCodeCacheFlushing
JRUBY_OPTS: --dev
JRUBY_OPTS: --dev -J-Djruby.thread.pool.enabled=true
MALLOC_ARENA_MAX: 2
RUBY_GC_HEAP_FREE_SLOTS: 600000
RUBY_GC_HEAP_GROWTH_FACTOR: 1.1
Expand All @@ -45,6 +45,8 @@ jobs:
ruby-version: 3.4

- run: bin/rake test
env:
SCREENSHOT_DRIVER: vips

functional-test:
name: Functional Test
Expand Down Expand Up @@ -87,11 +89,11 @@ jobs:
github.event.pull_request.requested_reviewers.length > 0
needs: [ functional-test ]
runs-on: ubuntu-latest
timeout-minutes: ${{ contains(matrix.ruby-version, 'jruby') && 12 || 8 }}
timeout-minutes: ${{ contains(matrix.ruby-version, 'jruby') && 20 || 8 }}
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
ruby-version: [ 3.4, 3.3, 3.2, jruby ]
ruby-version: [ 3.4, 3.3, 3.2, jruby-9.4, jruby-10.0 ]
gemfile:
- rails70_gems.rb
- rails71_gems.rb
Expand All @@ -104,7 +106,7 @@ jobs:
gemfile: rails80_gems.rb
experimental: false
# JRuby 9.x is Ruby 3.1 compatible, and Rails 8 requires Ruby 3.2.
- ruby-version: jruby
- ruby-version: jruby-9.4
gemfile: rails80_gems.rb
experimental: false
include:
Expand All @@ -130,7 +132,7 @@ jobs:
- name: Run tests (with 2 retries)
uses: nick-fields/retry@v3
with:
timeout_minutes: ${{ contains(matrix.ruby-version, 'jruby') && 3 || 3 }}
timeout_minutes: ${{ contains(matrix.ruby-version, 'jruby') && 7 || 3 }}
max_attempts: 3
command: bin/rake test

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*~
/.bundle/
/.idea
/.windsurf
/.yardoc
/_yardoc/
/coverage/
Expand Down
2 changes: 1 addition & 1 deletion .standard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
fix: true # default: false
parallel: true # default: false
format: progress # default: Standard::Formatter
ruby_version: 3.2 # default: RUBY_VERSION
ruby_version: 3.1 # to support JRuby 9.4
default_ignores: false # default: true

ignore: # default: []
Expand Down
30 changes: 13 additions & 17 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
# $ docker build . -t csd
# $ docker run -v $(pwd):/app -ti csd rake test

FROM --platform=linux/amd64 jetthoughts/cimg-ruby:3.4-chrome
FROM jetthoughts/cimg-ruby:3.4-chrome

# Install dependencies and clean up in one layer to reduce image size
RUN sudo apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive sudo apt-get install -qq \
ENV DEBIAN_FRONTEND=noninteractive \
BUNDLE_PATH=/bundle

RUN --mount=type=cache,target=/var/cache/apt \
sudo sed -i 's|http://security.ubuntu.com/ubuntu|http://archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list && \
sudo apt-get update -qq && \
sudo apt-get install -qq --fix-missing \
automake \
build-essential \
curl \
Expand All @@ -34,20 +38,12 @@ RUN sudo apt-get update -qq && \
libwebp-dev \
libxml2-dev \
swig && \
sudo apt-get autoremove -y && \
sudo apt-get autoclean && \
sudo apt-get clean && \
sudo rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
sudo apt-get autoclean

WORKDIR /app
COPY gems.rb gemfiles capybara-screenshot-diff.gemspec /app/
COPY lib/capybara/screenshot/diff/version.rb /app/lib/capybara/screenshot/diff/
RUN sudo sed -i 's/true/false/g' /etc/fonts/conf.d/10-antialias.conf

# Set the location for Bundler to store gems
ENV BUNDLE_PATH=/bundle

RUN sudo mkdir /bundle && \
sudo chmod a+w+r /bundle \
sudo mkdir -p /tmp/.X11-unix && \
sudo chmod 1777 /tmp/.X11-unix
RUN sudo mkdir -p /bundle /tmp/.X11-unix && \
sudo chmod 1777 /bundle /tmp/.X11-unix

WORKDIR /app
33 changes: 28 additions & 5 deletions bin/dtest
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
#!/bin/bash

set -eo pipefail
set -o pipefail

export DOCKER_DEFAULT_PLATFORM=linux/amd64

# Define allowed environment variables to pass to Docker
ALLOWED_ENV_VARS=(
"CI" "DEBUG" "TEST_ENV" "RAILS_ENV" "RACK_ENV" "COVERAGE" "DISABLE_ROLLBACK_COMPARISON_RUNTIME_FILES"
"RECORD_SCREENSHOTS" "TEST" "TESTOPTS" "SCREENSHOT_DRIVER"
)

# Build the Docker env args string
DOCKER_ENV_ARGS=""
for var in "${ALLOWED_ENV_VARS[@]}"; do
if [[ -n "${!var}" ]]; then
DOCKER_ENV_ARGS="$DOCKER_ENV_ARGS -e $var=${!var}"
fi
done

# Build the Docker image
docker build . -t csd:test

docker run -v ${PWD}:/app -v csd-bundle-cache:/bundle --rm -it csd:test bin/setup
# Run setup
(docker run $DOCKER_ENV_ARGS -v ${PWD}:/app -v csd-bundle-cache:/bundle --rm -it csd:test bin/setup) || exit 1

# Run tests with different drivers
echo "Running tests..."
docker run -e CAPYBARA_DRIVER=cuprite -v ${PWD}:/app -v csd-bundle-cache:/bundle --rm -it csd:test bin/rake test
docker run -e CAPYBARA_DRIVER=selenium_chrome_headless -v ${PWD}:/app -v csd-bundle-cache:/bundle --rm -it csd:test bin/rake test
docker run -e CAPYBARA_DRIVER=selenium_headless -v ${PWD}:/app -v csd-bundle-cache:/bundle --rm -it csd:test bin/rake test
DRIVERS=("cuprite" "selenium_chrome_headless" "selenium_headless")
for driver in "${DRIVERS[@]}"; do
echo "Running tests with $driver driver..."
docker run $DOCKER_ENV_ARGS -e CAPYBARA_DRIVER="$driver" \
-v ${PWD}:/app -v csd-bundle-cache:/bundle --rm -it csd:test \
bin/rake test "$@"

CAPYBARA_DRIVER="$driver" bin/rake test "$@"
done
3 changes: 2 additions & 1 deletion capybara-screenshot-diff.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_runtime_dependency "actionpack", ">= 7.0", "< 9"
spec.add_development_dependency "actionpack", ">= 7.0", "< 9"
spec.add_development_dependency "activesupport", ">= 7.0", "< 9"
spec.add_runtime_dependency "capybara", ">= 2", "< 4"
end
1 change: 1 addition & 0 deletions gemfiles/edge_gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
eval File.read(gems), binding, gems

git "https://github.com/rails/rails.git" do
gem "activesupport"
gem "actionpack"
end
1 change: 1 addition & 0 deletions gemfiles/rails70_gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
eval File.read(gems), binding, gems

gem "actionpack", "~> 7.0.0"
gem "activesupport", "~> 7.0.0", require: %w[active_support/deprecator active_support/test_case]
gem "mutex_m"
gem "drb"
gem "bigdecimal"
3 changes: 2 additions & 1 deletion gemfiles/rails71_gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
gems = "#{File.dirname __dir__}/gems.rb"
eval File.read(gems), binding, gems

gem "actionpack", "~> 7.1.0"
gem "activesupport", "~> 7.1.0", require: %w[logger active_support/deprecator active_support]
gem "actionpack", "~> 7.1.0", require: %w[action_controller action_dispatch]
1 change: 1 addition & 0 deletions gemfiles/rails80_gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
gems = "#{File.dirname __dir__}/gems.rb"
eval File.read(gems), binding, gems

gem "activesupport", "~> 8.0.0"
gem "actionpack", "~> 8.0.0"
29 changes: 29 additions & 0 deletions lib/capybara/screenshot/diff/capture_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Capybara
module Screenshot
module Diff
# Abstract base class for all screenshot‐capture strategies.
# A concrete strategy receives the raw capture/comparison option hashes,
# leaving them intact for now (we will introduce typed option objects in a
# later phase). It must implement `#take_comparison_screenshot` accepting
# a Snap.
class CaptureStrategy
def initialize(capture_options, comparison_options)
@capture_options = capture_options
@comparison_options = comparison_options
end

# @param snapshot [CapybaraScreenshotDiff::Snap]
# @return [void]
def take_comparison_screenshot(_snapshot)
raise NotImplementedError, "subclass responsibility"
end

private

attr_reader :capture_options, :comparison_options
end
end
end
end
3 changes: 3 additions & 0 deletions lib/capybara/screenshot/diff/comparison.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@

module Capybara::Screenshot::Diff
class Comparison < Struct.new(:new_image, :base_image, :options, :driver, :new_image_path, :base_image_path)
def skip_area
options[:skip_area]
end
end
end
33 changes: 33 additions & 0 deletions lib/capybara/screenshot/diff/comparison_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module Capybara
module Screenshot
module Diff
# Loads and preprocesses images for comparison
#
# This class is responsible for loading images and creating a Comparison object.
# It coordinates with the ImagePreprocessor to apply any necessary filters
# before creating the comparison. This follows the Single Responsibility Principle
# by focusing solely on loading and assembling the comparison.
class ComparisonLoader
def initialize(driver)
@driver = driver
end

# Load images and create a comparison object
# @param [String] base_path the path to the base image
# @param [String] new_path the path to the new image
# @param [Hash] options options for the comparison
# @return [Comparison] the comparison object
def call(base_path, new_path, options = {})
# Load the raw images
base_img, new_img = @driver.load_images(base_path, new_path)

# Create a preliminary comparison with raw images
# This is used for enhanced preprocessing that needs context
Comparison.new(new_img, base_img, options, @driver, new_path, base_path)
end
end
end
end
end
39 changes: 38 additions & 1 deletion lib/capybara/screenshot/diff/difference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,31 @@
module Capybara
module Screenshot
module Diff
class Difference < Struct.new(:region, :meta, :comparison, :failed_by)
# Represents a difference between two images
#
# This value object encapsulates the result of an image comparison operation.
# It follows the Single Responsibility Principle by focusing solely on representing
# the difference state, including:
# - Whether images are different or equal
# - Why they differ (dimensions, pixels, etc.)
# - The specific region of difference
# - Whether differences are tolerable based on configured thresholds
#
# As part of the layered comparison architecture, this class represents the final
# output of the comparison process, containing all data needed for reporting.
# Represents a difference between two images
class Difference < Struct.new(:region, :meta, :comparison, :failed_by, :base_image_path, :image_path, keyword_init: nil)
def self.build_null(comparison, base_image_path, new_image_path, failed_by = nil)
Difference.new(
nil,
{difference_level: nil, max_color_distance: 0},
comparison,
failed_by,
base_image_path,
new_image_path
).freeze
end

def different?
failed? || !(blank? || tolerable?)
end
Expand Down Expand Up @@ -61,6 +85,19 @@ def inspect
def tolerable?
!!((area_size_limit && area_size_limit >= region_area_size) || (tolerance && tolerance >= ratio))
end

# Path accessors for backward compatibility
def new_image_path
image_path || comparison&.new_image_path
end

def original_image_path
base_image_path || comparison&.base_image_path
end

def diff_mask
meta[:diff_mask]
end
end
end
end
Expand Down
Loading