Skip to content

Instantly share code, notes, and snippets.

@mhluska
Last active June 10, 2018 12:52
Show Gist options
  • Save mhluska/86f302d2fc1b4356db814aa9408aedba to your computer and use it in GitHub Desktop.
Save mhluska/86f302d2fc1b4356db814aa9408aedba to your computer and use it in GitHub Desktop.
Quick and dirty visual regression testing with Rails + Capybara + Selenium
# spec/rails_helper.rb
if ENV['SAVE_SCREENSHOTS']
module CapybaraElementExtensions
INTERACTION_METHODS = %i[set select_option unselect_option click
right_click double_click send_keys hover trigger drag_to execute_script
evaluate_script evaluate_async_script]
INTERACTION_METHODS.each do |method|
define_method method do |*args, &block|
result = super(*args, &block)
sleep 1
save_screenshot_with_description_filename
result
end
end
private
def screenshots_type
ENV['SAVE_SCREENSHOTS'] == 'baseline' ? 'baseline' : 'after'
end
def feature_name
RSpec.current_example.example_group.description.parameterize.underscore
end
def test_name
RSpec.current_example.description.parameterize.underscore
end
def save_screenshot_with_description_filename
self.class.interaction_counters[test_name] ||= 0
filename = self.class.interaction_counters[test_name].to_s + '.png'
path = Rails.root.join('spec', 'screenshots', screenshots_type, feature_name, test_name, filename)
Capybara.save_screenshot(path)
self.class.interaction_counters[test_name] += 1
end
end
module Capybara
module Node
class Element
class << self
attr_accessor :interaction_counters
end
@interaction_counters = {}
prepend CapybaraElementExtensions
end
end
end
end
# lib/visual_regression.rb
class VisualRegression
def self.write_html(image_paths)
images_html = image_paths.map { |path| "<div style='margin-bottom: 2em;'><a href=\"file://#{path}\" target=\"_blank\">#{path}</a></div><img src=\"#{path}\" style='max-width: 100%; margin-bottom: 4em; border-bottom: 1px solid #ddd; padding-bottom: 4em;' />" }.join("\n")
html = "<!DOCTYPE html><html><head><title></title></head><body style='font-family: sans-serif;'>#{images_html}</body></html>"
path = Rails.root.join('spec', 'screenshots', 'index.html')
File.write(path, html)
puts "Wrote #{path}"
end
def self.run
root_path = Rails.root.join('spec', 'screenshots', 'baseline')
image_paths = []
Find.find(root_path) do |path|
if File.basename(path)[0] == '.'
Find.prune
next
end
next if FileTest.directory?(path)
baseline = Magick::ImageList.new(path)
after_path = path.gsub('/baseline/', '/after/')
after = begin
Magick::ImageList.new(after_path)
rescue Magick::ImageMagickError
puts "Not found, skipping #{after_path}"
next
end
diff, mean_error = baseline.compare_channel(after, Magick::MeanAbsoluteErrorMetric)
diff_path = path.gsub('/baseline/', '/diff/')
if mean_error > 0.01
puts "Error #{(mean_error * 100).round(2)}\% found for #{diff_path}"
FileUtils.mkdir_p(File.dirname(diff_path))
(after + baseline + [diff]).append(false).write(diff_path)
image_paths << diff_path
end
end
write_html(image_paths) unless image_paths.empty?
end
end
# lib/tasks/spec.rake
namespace :spec do
desc 'Run visial regression comparisons'
task :run_visual_regression => :environment do
VisualRegression.run
end
end
# .gitignore
/spec/screenshots/after
/spec/screenshots/diff
/spec/screenshots/index.html
@mhluska
Copy link
Author

mhluska commented Jun 10, 2018

Easily adds visual regression testing to your existing selenium test suite. It works by monkey-patching Capybara to take screenshots whenever any NODE_METHODS are run. Now just run your tests before and after any front end changes and compare the resulting screenshots with a rake task.


SAVE_SCREENSHOTS=baseline bundle exec rspec

Then take after screenshots using:

SAVE_SCREENSHOTS=after bundle exec rspec

Then run the diff task:

rake spec:run_visual_regression

This will generate diff screenshots with baseline and complain if any have a difference beyond a threshold. Inspect screenshots:

open spec/screenshots/index.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment