├── .rspec ├── .rvmrc ├── lib ├── green_onion │ ├── version.rb │ ├── errors.rb │ ├── drivers │ │ ├── selenium.rb │ │ ├── webkit.rb │ │ └── poltergeist.rb │ ├── browser.rb │ ├── generators │ │ └── skinner.erb │ ├── configuration.rb │ ├── cli.rb │ ├── screenshot.rb │ └── compare.rb └── green_onion.rb ├── bin └── green_onion ├── spec ├── skins │ ├── spec_shot.png │ ├── spec_shot_fresh.png │ └── spec_shot_resize.png ├── sample_app │ ├── public │ │ ├── onion_face_0.jpg │ │ └── onion_face_1.jpg │ └── sample_app.rb ├── spec_helper.rb └── unit │ ├── drivers │ ├── webkit_spec.rb │ ├── selenium_spec.rb │ └── poltergeist_spec.rb │ ├── compare_spec.rb │ ├── cli_spec.rb │ ├── screenshot_spec.rb │ └── green_onion_spec.rb ├── Gemfile ├── .travis.yml ├── .gitignore ├── LICENSE ├── green_onion.gemspec ├── Rakefile └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.2@green_onion --create 2 | -------------------------------------------------------------------------------- /lib/green_onion/version.rb: -------------------------------------------------------------------------------- 1 | module GreenOnion 2 | VERSION = "0.1.4" 3 | end 4 | -------------------------------------------------------------------------------- /bin/green_onion: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "green_onion/cli" 3 | 4 | GreenOnion::CLI.start -------------------------------------------------------------------------------- /spec/skins/spec_shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobomo/green_onion/HEAD/spec/skins/spec_shot.png -------------------------------------------------------------------------------- /spec/skins/spec_shot_fresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobomo/green_onion/HEAD/spec/skins/spec_shot_fresh.png -------------------------------------------------------------------------------- /spec/skins/spec_shot_resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobomo/green_onion/HEAD/spec/skins/spec_shot_resize.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in green_onion.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /spec/sample_app/public/onion_face_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobomo/green_onion/HEAD/spec/sample_app/public/onion_face_0.jpg -------------------------------------------------------------------------------- /spec/sample_app/public/onion_face_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobomo/green_onion/HEAD/spec/sample_app/public/onion_face_1.jpg -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'fileutils' 4 | require 'green_onion' 5 | 6 | RSpec.configure do |config| 7 | # some (optional) config here 8 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.2 4 | before_install: 5 | - sudo apt-get install libqt4-dev libqtwebkit-dev 6 | before_script: 7 | - "export DISPLAY=:99.0" 8 | - "sh -e /etc/init.d/xvfb start" 9 | script: rake spec -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /lib/green_onion/errors.rb: -------------------------------------------------------------------------------- 1 | module GreenOnion 2 | class Errors 3 | #Base class for all errors 4 | class Error < StandardError; end 5 | 6 | class IllformattedURL < Error; end 7 | 8 | class ThresholdOutOfRange < Error; end 9 | end 10 | end -------------------------------------------------------------------------------- /lib/green_onion/drivers/selenium.rb: -------------------------------------------------------------------------------- 1 | require 'capybara/dsl' 2 | 3 | module GreenOnion 4 | class Selenium 5 | include Capybara::DSL 6 | 7 | def initialize 8 | Capybara.default_driver = :selenium 9 | end 10 | 11 | def record(url, path, dimensions=nil) 12 | visit url 13 | page.driver.browser.save_screenshot(path) 14 | end 15 | 16 | end 17 | end -------------------------------------------------------------------------------- /lib/green_onion/drivers/webkit.rb: -------------------------------------------------------------------------------- 1 | require 'capybara/dsl' 2 | require 'capybara-webkit' 3 | 4 | module GreenOnion 5 | class Webkit 6 | include Capybara::DSL 7 | 8 | def initialize 9 | Capybara.default_driver = :webkit 10 | end 11 | 12 | def record(url, path, dimensions) 13 | visit url 14 | page.driver.render(path, dimensions) 15 | end 16 | 17 | end 18 | end -------------------------------------------------------------------------------- /lib/green_onion/drivers/poltergeist.rb: -------------------------------------------------------------------------------- 1 | require 'capybara/dsl' 2 | require 'capybara/poltergeist' 3 | 4 | module GreenOnion 5 | class Poltergeist 6 | include Capybara::DSL 7 | 8 | def initialize 9 | Capybara.default_driver = :poltergeist 10 | end 11 | 12 | def record(url, path, dimensions) 13 | visit url 14 | page.driver.resize(dimensions[:width], dimensions[:height]) 15 | page.driver.render(path, :full => true) 16 | end 17 | 18 | end 19 | end -------------------------------------------------------------------------------- /spec/sample_app/sample_app.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'sinatra' 3 | 4 | class SampleApp < Sinatra::Base 5 | set :root, File.dirname(__FILE__) 6 | set :static, true 7 | set :logging, false 8 | 9 | get '/' do 10 | "
" 11 | end 12 | 13 | get "/fake_uri" do 14 | "

foo

" 15 | end 16 | 17 | get "/onion_face" do 18 | "" 19 | end 20 | 21 | get "/another/uri/string" do 22 | "

It was the best of times, it was the blorst of times.

" 23 | end 24 | 25 | end -------------------------------------------------------------------------------- /lib/green_onion/browser.rb: -------------------------------------------------------------------------------- 1 | module GreenOnion 2 | class Browser 3 | 4 | attr_reader :driver, :dimensions 5 | 6 | def initialize(params={}) 7 | @driver = params[:driver] 8 | @dimensions = params[:dimensions] 9 | load_driver 10 | end 11 | 12 | def load_driver 13 | begin 14 | require "green_onion/drivers/#{driver}" 15 | @driver_obj = GreenOnion.const_get(@driver.capitalize).new 16 | rescue LoadError => e 17 | raise e unless e.message.include?("green_onion/drivers") 18 | raise ArgumentError.new("#{@driver} is not supported by GreenOnion.") 19 | end 20 | end 21 | 22 | def snap_screenshot(url, path) 23 | @driver_obj.record(url, path, @dimensions) 24 | end 25 | 26 | end 27 | end -------------------------------------------------------------------------------- /lib/green_onion/generators/skinner.erb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'green_onion' 5 | 6 | ENV["RAILS_ENV"] ||= 'test' 7 | require File.expand_path("../../config/environment", __FILE__) 8 | 9 | GreenOnion.configure do |c| 10 | # You can use and customize these configuration options 11 | # c.skins_dir = "#{::Rails.root}/spec/skins" 12 | # c.threshold = 100 13 | # c.dimensions = { :width => 1024, :height => 768 } 14 | # c.skin_name = { 15 | # :match => /[\/]/, 16 | # :replace => "_", 17 | # :prefix => nil, 18 | # :root => "root" 19 | # } 20 | end 21 | 22 | all_routes = Rails.application.routes.routes 23 | routes = all_routes.collect { |r| r.path.spec.to_s.gsub(/\/*(\(\.)*:(\w*)(\))*\/*/, "") }.delete_if(&:empty?) 24 | 25 | routes.each do |route| 26 | GreenOnion.skin_visual_and_percentage("<%= config[:url] %>" + route) 27 | end -------------------------------------------------------------------------------- /spec/unit/drivers/webkit_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'Using Webkit' do 4 | 5 | before(:all) do 6 | @url = 'http://localhost:8070' 7 | @url_w_uri = @url + '/fake_uri' 8 | @tmp_path = './spec/tmp' 9 | @browser = GreenOnion::Browser.new( 10 | :dimensions => { :width => 1024, :height => 768 }, 11 | :driver => "webkit" 12 | ) 13 | end 14 | 15 | before(:each) do 16 | @screenshot = GreenOnion::Screenshot.new( 17 | :browser => @browser, 18 | :dir => @tmp_path, 19 | :skin_name => { 20 | :match => /[\/]/, 21 | :replace => "_", 22 | :prefix => nil, 23 | :root => "root" 24 | } 25 | ) 26 | @file = "#{@tmp_path}/fake_uri.png" 27 | end 28 | 29 | after(:each) do 30 | FileUtils.rm_r(@tmp_path, :force => true) 31 | end 32 | 33 | it "should snap and save screenshot w/ Webkit" do 34 | @screenshot.test_screenshot(@url_w_uri) 35 | File.exist?(@file).should be_true 36 | end 37 | end -------------------------------------------------------------------------------- /spec/unit/drivers/selenium_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'Using Selenium' do 4 | 5 | before(:all) do 6 | @url = 'http://localhost:8070' 7 | @url_w_uri = @url + '/fake_uri' 8 | @tmp_path = './spec/tmp' 9 | @browser = GreenOnion::Browser.new( 10 | :dimensions => { :width => 1024, :height => 768 }, 11 | :driver => "selenium" 12 | ) 13 | end 14 | 15 | before(:each) do 16 | @screenshot = GreenOnion::Screenshot.new( 17 | :browser => @browser, 18 | :dir => @tmp_path, 19 | :skin_name => { 20 | :match => /[\/]/, 21 | :replace => "_", 22 | :prefix => nil, 23 | :root => "root" 24 | } 25 | ) 26 | @file = "#{@tmp_path}/fake_uri.png" 27 | end 28 | 29 | after(:each) do 30 | FileUtils.rm_r(@tmp_path, :force => true) 31 | end 32 | 33 | it "should snap and save screenshot w/ Selenium" do 34 | @screenshot.test_screenshot(@url_w_uri) 35 | File.exist?(@file).should be_true 36 | end 37 | end -------------------------------------------------------------------------------- /spec/unit/drivers/poltergeist_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'Using Poltergeist' do 4 | 5 | before(:all) do 6 | @url = 'http://localhost:8070' 7 | @url_w_uri = @url + '/fake_uri' 8 | @tmp_path = './spec/tmp' 9 | @browser = GreenOnion::Browser.new( 10 | :dimensions => { :width => 1024, :height => 768 }, 11 | :driver => "poltergeist" 12 | ) 13 | end 14 | 15 | before(:each) do 16 | @screenshot = GreenOnion::Screenshot.new( 17 | :browser => @browser, 18 | :dir => @tmp_path, 19 | :skin_name => { 20 | :match => /[\/]/, 21 | :replace => "_", 22 | :prefix => nil, 23 | :root => "root" 24 | } 25 | ) 26 | @file = "#{@tmp_path}/fake_uri.png" 27 | end 28 | 29 | after(:each) do 30 | FileUtils.rm_r(@tmp_path, :force => true) 31 | end 32 | 33 | it "should snap and save screenshot w/ Poltergeist" do 34 | @screenshot.test_screenshot(@url_w_uri) 35 | File.exist?(@file).should be_true 36 | end 37 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Ted O'Meara 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /spec/unit/compare_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GreenOnion::Compare do 4 | 5 | describe 'Comparing Screenshots' do 6 | 7 | before(:each) do 8 | @comparison = GreenOnion::Compare.new 9 | @spec_shot1 = './spec/skins/spec_shot.png' 10 | @spec_shot2 = './spec/skins/spec_shot_fresh.png' 11 | @spec_shot_resize = './spec/skins/spec_shot_resize.png' 12 | @diff_shot = './spec/skins/spec_shot_diff.png' 13 | end 14 | 15 | after(:all) do 16 | FileUtils.rm('./spec/skins/spec_shot_diff.png', :force => true) 17 | end 18 | 19 | it 'should get a percentage of difference between two shots' do 20 | @comparison.percentage_diff(@spec_shot1, @spec_shot2) 21 | @comparison.percentage_changed.should eq(66.0) 22 | end 23 | 24 | it 'should create a new file with a visual diff between two shots' do 25 | @comparison.visual_diff(@spec_shot1, @spec_shot2) 26 | File.exist?(@diff_shot).should be_true 27 | end 28 | 29 | it "should not throw error when dimensions are off" do 30 | expect { @comparison.visual_diff(@spec_shot1, @spec_shot_resize) }.to_not raise_error 31 | end 32 | 33 | end 34 | end -------------------------------------------------------------------------------- /green_onion.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/green_onion/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Ted O'Meara"] 6 | gem.email = ["ted@intridea.com"] 7 | gem.description = %q{UI testing/screenshot diffing tool} 8 | gem.summary = %q{Regressions in the view making you cry? Have more confidence with GreenOnion.} 9 | gem.homepage = "http://intridea.github.com/green_onion" 10 | 11 | gem.add_development_dependency "rake" 12 | gem.add_development_dependency "rspec" 13 | gem.add_development_dependency "sinatra" 14 | gem.add_development_dependency "capybara-webkit" 15 | gem.add_development_dependency "poltergeist" 16 | 17 | gem.add_dependency "capybara", " ~> 1.1" 18 | gem.add_dependency "oily_png", "~> 1.0.2" 19 | gem.add_dependency "rainbow" 20 | gem.add_dependency "fileutils" 21 | gem.add_dependency "thor", ">= 0.14.6" 22 | 23 | gem.files = `git ls-files`.split($\) 24 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 25 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 26 | gem.name = "green_onion" 27 | gem.require_paths = ["lib"] 28 | gem.version = GreenOnion::VERSION 29 | end 30 | -------------------------------------------------------------------------------- /lib/green_onion/configuration.rb: -------------------------------------------------------------------------------- 1 | module GreenOnion 2 | class Configuration 3 | 4 | attr_writer :threshold, :skins_dir, :driver 5 | 6 | def dimensions=(options) 7 | @dimensions = options 8 | end 9 | 10 | def dimensions 11 | @dimensions ||= { :height => 768, :width => 1024 } 12 | end 13 | 14 | def threshold 15 | @threshold ||= 100 16 | end 17 | 18 | def skins_dir 19 | @skins_dir ||= './spec/skins' 20 | end 21 | 22 | def driver 23 | @driver ||= :webkit 24 | end 25 | 26 | # Uses the driver and dimensions configuration vars to return a Browser object 27 | def browser 28 | @browser = Browser.new( 29 | :dimensions => dimensions, 30 | :driver => driver 31 | ) 32 | end 33 | 34 | def skin_name=(options) 35 | @skin_name = skin_namespace_hash(options) 36 | end 37 | 38 | def skin_name 39 | @skin_name ||= skin_namespace_hash 40 | end 41 | 42 | # Serves as a template for skin_name getter/setter 43 | def skin_namespace_hash(options = {}) 44 | { 45 | :match => options[:match] ? options[:match] : /[\/]/, 46 | :replace => options[:replace] ? options[:replace] : "_", 47 | :prefix => options[:prefix] ? options[:prefix] : nil, 48 | :root => options[:root] ? options[:root] : "root" 49 | } 50 | end 51 | 52 | end 53 | end -------------------------------------------------------------------------------- /spec/unit/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "bin/green_onion" do 4 | 5 | before(:all) do 6 | @tmp_path = './spec/tmp' 7 | @url = 'http://localhost:8070' 8 | @file1 = "#{@tmp_path}/root.png" 9 | @skinner_file = "#{@tmp_path}/skinner.rb" 10 | end 11 | 12 | describe "Skin Utility" do 13 | 14 | after(:each) do 15 | FileUtils.rm_r(@tmp_path, :force => true) 16 | end 17 | 18 | it "should run the skin task w/o any flags (need the --dir flag to keep spec directory clean)" do 19 | `bin/green_onion skin #{@url} -d=#{@tmp_path}` 20 | File.exist?(@file1).should be_true 21 | end 22 | 23 | it "should run the skin task w/ --method=p flag to run only percentage diff" do 24 | stdin, stdout, stderr = Open3.popen3("bin/green_onion skin #{@url} --dir=#{@tmp_path} --method=p --threshold=1 && 25 | bin/green_onion skin #{@url} --dir=#{@tmp_path} --method=p --threshold=1") 26 | stderr.readlines.to_s.should include("above threshold set @") 27 | end 28 | 29 | end 30 | 31 | describe "Generator" do 32 | 33 | after(:each) do 34 | FileUtils.rm_r(@tmp_path, :force => true) 35 | end 36 | 37 | it "should build the skinner file" do 38 | `bin/green_onion generate --dir=#{@tmp_path}` 39 | File.exist?(@skinner_file).should be_true 40 | end 41 | 42 | it "should build the skinner file with the url included correctly" do 43 | `bin/green_onion generate --url=#{@url} --dir=#{@tmp_path}` 44 | skinner = IO.read(@skinner_file) 45 | skinner.should include("GreenOnion.skin_visual_and_percentage(\"http://localhost:8070\" + route)") 46 | end 47 | 48 | end 49 | end -------------------------------------------------------------------------------- /lib/green_onion/cli.rb: -------------------------------------------------------------------------------- 1 | require "thor" 2 | require "green_onion" 3 | 4 | module GreenOnion 5 | class CLI < Thor 6 | include Thor::Actions 7 | 8 | source_root File.expand_path('../generators', __FILE__) 9 | 10 | class_option :dir, :aliases => "-d", :type => :string 11 | 12 | desc "skin ", "Creates skins from and compares them" 13 | method_option :method, :aliases => "-m", :type => :string 14 | method_option :threshold, :aliases => "-t", :type => :numeric 15 | method_option :browser, :aliases => "-b", :type => :string 16 | method_option :width, :aliases => "-w", :type => :numeric 17 | method_option :height, :aliases => "-h", :type => :numeric 18 | def skin(url) 19 | GreenOnion.configure do |c| 20 | c.skins_dir = options[:dir] if options[:dir] 21 | c.threshold = options[:threshold] if options[:threshold] 22 | c.dimensions = { :width => options[:width], :height => options[:height] } if options[:width] && options[:height] 23 | c.driver = options[:driver].to_sym if options[:driver] 24 | end 25 | case options[:method] 26 | when "v" 27 | GreenOnion.skin_visual(url) 28 | when "p" 29 | GreenOnion.skin_percentage(url) 30 | else 31 | GreenOnion.skin_visual_and_percentage(url) 32 | end 33 | end 34 | 35 | desc "generate", "Generates a 'skinner' file to test only Rails routes without params" 36 | method_option :url, :aliases => "-u", :type => :string 37 | def generate_skinner 38 | options[:dir] ? dir = options[:dir] : dir = "spec" 39 | options[:url] ? config = { :url => options[:url] } : config = { :url => "http://localhost:3000" } 40 | template('skinner.erb', "#{dir}/skinner.rb", config) 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require 'rack' 4 | 5 | desc "Running server..." 6 | task :server do 7 | require File.dirname(__FILE__) + '/spec/sample_app/sample_app.rb' 8 | Rack::Handler::WEBrick.run SampleApp, :Port => 8070 9 | end 10 | 11 | desc "Running specs..." 12 | task :specs do 13 | system "rspec spec" 14 | end 15 | 16 | desc "Running specs on test server" 17 | task :spec do 18 | # have the server run on its own thread so that it doesn't block the spec task 19 | server = Thread.new do 20 | task("server").execute 21 | end 22 | task("specs").execute 23 | server.kill 24 | end 25 | 26 | namespace :benchmarks do 27 | require 'benchmark' 28 | task("server").execute 29 | @tmp_path = './spec/tmp' 30 | 31 | desc "WebKit benchmark" 32 | task :webkit do 33 | require File.expand_path('../lib/green_onion', __FILE__) 34 | GreenOnion.configure do |c| 35 | c.skins_dir = @tmp_path 36 | c.driver = "webkit" 37 | end 38 | Benchmark.bm do |x| 39 | x.report("webkit:") { 2.times do; GreenOnion.skin_visual('http://localhost:8070'); end } 40 | end 41 | FileUtils.rm_r(@tmp_path, :force => true) 42 | end 43 | 44 | desc "PhantomJS benchmark" 45 | task :phantomjs do 46 | require File.expand_path('../lib/green_onion', __FILE__) 47 | GreenOnion.configure do |c| 48 | c.skins_dir = @tmp_path 49 | c.driver = "poltergeist" 50 | end 51 | Benchmark.bm do |x| 52 | x.report("poltergeist:") { 2.times do; GreenOnion.skin_visual('http://localhost:8070'); end } 53 | end 54 | FileUtils.rm_r(@tmp_path, :force => true) 55 | end 56 | 57 | desc "Selenium benchmark" 58 | task :selenium do 59 | require File.expand_path('../lib/green_onion', __FILE__) 60 | GreenOnion.configure do |c| 61 | c.skins_dir = @tmp_path 62 | c.driver = "selenium" 63 | end 64 | Benchmark.bm do |x| 65 | x.report("selenium:") { 2.times do; GreenOnion.skin_visual('http://localhost:8070'); end } 66 | end 67 | FileUtils.rm_r(@tmp_path, :force => true) 68 | end 69 | end -------------------------------------------------------------------------------- /lib/green_onion/screenshot.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | module GreenOnion 4 | class Screenshot 5 | 6 | attr_reader :paths_hash, :browser, :dir, :skin_name, :dimensions 7 | 8 | def initialize(params={}) 9 | @dir = params[:dir] 10 | @skin_name = params[:skin_name] 11 | @browser = params[:browser] 12 | @paths_hash = {} 13 | end 14 | 15 | def test_screenshot(url) 16 | url_to_path(url) 17 | create_dir(@dir) 18 | @browser.snap_screenshot(url, @shot_path) 19 | end 20 | 21 | def url_to_path(url) 22 | get_path(url) 23 | if File.exist?(@paths_hash[:original]) 24 | @paths_hash[:fresh] = @paths_hash[:original].dup.insert(-5, '_fresh') 25 | @shot_path = @paths_hash[:fresh] 26 | else 27 | @shot_path = @paths_hash[:original] 28 | end 29 | end 30 | 31 | def url_matcher(url) 32 | url_match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/).to_a.compact 33 | if url_match.length >= 5 34 | @filename = url_match[5] 35 | else 36 | raise Errors::IllformattedURL.new "Your URL is incorrectly formatted. Please make sure to use http://" 37 | end 38 | end 39 | 40 | def file_namer 41 | @filename.slice!(/^\//) # remove the beginning "/" if there is one 42 | @filename = @filename.gsub(@skin_name[:match], @skin_name[:replace]) # by default, all "/" in a URI string will be replaced with "_" 43 | @filename = @skin_name[:prefix] + @filename if @skin_name[:prefix] # add on a prefix defined in the configuration block 44 | @paths_hash[:original] = "#{@dir}/#{@filename}.png" 45 | end 46 | 47 | def create_dir(dir) 48 | unless Dir.exist?(dir) 49 | FileUtils.mkdir(dir) 50 | end 51 | end 52 | 53 | def get_path(url) 54 | url_matcher(url) 55 | if @filename.empty? || @filename == '/' 56 | @paths_hash[:original] = "#{@dir}/#{@skin_name[:root]}.png" 57 | else 58 | file_namer 59 | end 60 | end 61 | 62 | def destroy(url) 63 | get_path(url) 64 | destroy_files(@paths_hash[:original], @paths_hash[:fresh]) 65 | end 66 | 67 | def destroy_files(org, fresh) 68 | if File.exist?(org) 69 | FileUtils.rm(org) 70 | if File.exist?(fresh) 71 | FileUtils.rm(fresh) 72 | end 73 | end 74 | end 75 | 76 | end 77 | end -------------------------------------------------------------------------------- /lib/green_onion/compare.rb: -------------------------------------------------------------------------------- 1 | require "oily_png" 2 | require "rainbow" 3 | 4 | module GreenOnion 5 | class Compare 6 | 7 | attr_accessor :percentage_changed, :total_px, :changed_px 8 | attr_reader :diffed_image 9 | 10 | # Pulled from Jeff Kreeftmeijer's post here: http://jeffkreeftmeijer.com/2011/comparing-images-and-creating-image-diffs/ 11 | # Thanks Jeff! 12 | def diff_images(org, fresh) 13 | @images = [ 14 | ChunkyPNG::Image.from_file(org), 15 | ChunkyPNG::Image.from_file(fresh) 16 | ] 17 | 18 | @diff_index = [] 19 | begin 20 | diff_iterator 21 | rescue ChunkyPNG::OutOfBounds 22 | warn "Skins are different sizes. Please delete #{org} and/or #{fresh}.".color(:yellow) 23 | end 24 | end 25 | 26 | # Run through all of the pixels on both org image, and fresh image. Change the pixel color accordingly. 27 | def diff_iterator 28 | @images.first.height.times do |y| 29 | @images.first.row(y).each_with_index do |pixel, x| 30 | unless pixel == @images.last[x,y] 31 | @diff_index << [x,y] 32 | pixel_difference_filter(pixel, x, y) 33 | end 34 | end 35 | end 36 | end 37 | 38 | # Changes the pixel color to be the opposite RGB value 39 | def pixel_difference_filter(pixel, x, y) 40 | chans = [] 41 | [:r, :b, :g].each do |chan| 42 | chans << channel_difference(chan, pixel, x, y) 43 | end 44 | @images.last[x,y] = ChunkyPNG::Color.rgb(chans[0], chans[1], chans[2]) 45 | end 46 | 47 | # Interface to run the R, G, B methods on ChunkyPNG 48 | def channel_difference(chan, pixel, x, y) 49 | ChunkyPNG::Color.send(chan, pixel) + ChunkyPNG::Color.send(chan, @images.last[x,y]) - 2 * [ChunkyPNG::Color.send(chan, pixel), ChunkyPNG::Color.send(chan, @images.last[x,y])].min 50 | end 51 | 52 | # Returns the numeric results of the diff of 2 images 53 | def percentage_diff(org, fresh) 54 | diff_images(org, fresh) 55 | @total_px = @images.first.pixels.length 56 | @changed_px = @diff_index.length 57 | @percentage_changed = ( (@diff_index.length.to_f / @images.first.pixels.length) * 100 ).round(2) 58 | end 59 | 60 | # Returns the visual results of the diff of 2 images 61 | def visual_diff(org, fresh) 62 | diff_images(org, fresh) 63 | save_visual_diff(org, fresh) 64 | end 65 | 66 | # Saves the visual diff as a separate file 67 | def save_visual_diff(org, fresh) 68 | x, y = @diff_index.map{ |xy| xy[0] }, @diff_index.map{ |xy| xy[1] } 69 | @diffed_image = org.insert(-5, '_diff') 70 | 71 | begin 72 | @images.last.rect(x.min, y.min, x.max, y.max, ChunkyPNG::Color.rgb(0,255,0)) 73 | rescue NoMethodError 74 | puts "Both skins are the same.".color(:yellow) 75 | end 76 | 77 | @images.last.save(@diffed_image) 78 | end 79 | 80 | end 81 | end -------------------------------------------------------------------------------- /lib/green_onion.rb: -------------------------------------------------------------------------------- 1 | require "green_onion/version" 2 | require "green_onion/screenshot" 3 | require "green_onion/compare" 4 | require "green_onion/configuration" 5 | require "green_onion/errors" 6 | require "green_onion/browser" 7 | require "rainbow" 8 | 9 | module GreenOnion 10 | class << self 11 | 12 | attr_reader :compare, :screenshot 13 | 14 | # Pass configure block to set Configuration object 15 | def configure 16 | yield configuration 17 | end 18 | 19 | def configuration 20 | @configuration ||= Configuration.new 21 | end 22 | 23 | # Bring the Screenshot and Compare classes together to create a skin 24 | def skin(url) 25 | @screenshot = Screenshot.new( 26 | :dir => @configuration.skins_dir, 27 | :skin_name => @configuration.skin_name, 28 | :browser => @configuration.browser 29 | ) 30 | @compare = Compare.new 31 | 32 | @screenshot.test_screenshot(url) 33 | end 34 | 35 | # Finds the percentage of change between skins 36 | # Threshold can be set in configuration, or as an argument itself, and can be specific to an instance 37 | def skin_percentage(url, threshold=@configuration.threshold) 38 | raise Errors::ThresholdOutOfRange.new "The threshold need to be a number between 1 and 100" if threshold > 100 39 | skin(url) 40 | skin_picker(url, { :percentage => true }, threshold) 41 | end 42 | 43 | # Creates a diffed screenshot between skins 44 | def skin_visual(url) 45 | skin(url) 46 | skin_picker(url, { :visual => true }) 47 | end 48 | 49 | # Creates a diffed screenshot between skins AND prints percentage changed 50 | def skin_visual_and_percentage(url, threshold=@configuration.threshold) 51 | raise Errors::ThresholdOutOfRange.new "The threshold need to be a number between 1 and 100" if threshold > 100 52 | skin(url) 53 | skin_picker(url, { :percentage => true, :visual => true }, threshold) 54 | end 55 | 56 | def skin_picker(url, type, threshold=100) 57 | if(@screenshot.paths_hash.length > 1) 58 | puts "\n" + url.color(:cyan) 59 | if type[:percentage] 60 | @compare.percentage_diff(@screenshot.paths_hash[:original], @screenshot.paths_hash[:fresh]) 61 | threshold_alert(@compare.percentage_changed, threshold) 62 | end 63 | if type[:visual] 64 | @compare.visual_diff(@screenshot.paths_hash[:original], @screenshot.paths_hash[:fresh]) 65 | end 66 | else 67 | puts "\n#{url}".color(:cyan) + " has been saved to #{@screenshot.paths_hash[:original]}".color(:yellow) 68 | end 69 | end 70 | 71 | # This is used in skin_percentage to raise error if a set of skins are ok or not 72 | def threshold_alert(actual, threshold) 73 | if actual > threshold 74 | abort "#{actual - threshold}% above threshold set @ #{threshold}%".color(:red) + 75 | "\npixels changed (%): #{@compare.percentage_changed}%" + 76 | "\npixels changed/total: #{@compare.changed_px}/#{@compare.total_px}" 77 | else 78 | puts "pixels changed/total: #{@compare.changed_px}/#{@compare.total_px}" 79 | end 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/unit/screenshot_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GreenOnion::Screenshot do 4 | 5 | before(:all) do 6 | @url = 'http://localhost:8070' 7 | @url_w_uri = @url + '/fake_uri' 8 | @tmp_path = './spec/tmp' 9 | @browser = GreenOnion::Browser.new( 10 | :dimensions => { :width => 1024, :height => 768 }, 11 | :driver => "webkit" 12 | ) 13 | end 14 | 15 | describe 'Snap single screenshot' do 16 | 17 | before(:each) do 18 | @screenshot = GreenOnion::Screenshot.new( 19 | :browser => @browser, 20 | :dir => @tmp_path, 21 | :skin_name => { 22 | :match => /[\/]/, 23 | :replace => "", 24 | :prefix => nil, 25 | :root => "root" 26 | } 27 | ) 28 | @file = "#{@tmp_path}/fake_uri.png" 29 | end 30 | 31 | after(:each) do 32 | FileUtils.rm_r(@tmp_path, :force => true) 33 | end 34 | 35 | it 'should build the path from the URI' do 36 | @screenshot.url_to_path(@url_w_uri).should eq(@file) 37 | end 38 | 39 | it 'should build the path from root' do 40 | @screenshot.url_to_path('http://localhost:8070').should eq("#{@tmp_path}/root.png") 41 | end 42 | 43 | it 'should build the path from root (even with trailing slash)' do 44 | @screenshot.url_to_path('http://localhost:8070/').should eq("#{@tmp_path}/root.png") 45 | end 46 | 47 | it 'should snap and save screenshot' do 48 | @screenshot.browser.snap_screenshot(@url_w_uri, @file) 49 | File.exist?(@file).should be_true 50 | end 51 | 52 | it "should destroy a singular screenshot" do 53 | @screenshot.destroy(@url_w_uri) 54 | File.exist?(@file).should be_false 55 | end 56 | end 57 | 58 | describe 'Snap two screenshots' do 59 | 60 | before(:each) do 61 | @screenshot = GreenOnion::Screenshot.new( 62 | :browser => @browser, 63 | :dir => @tmp_path, 64 | :skin_name => { 65 | :match => /[\/]/, 66 | :replace => "", 67 | :prefix => nil, 68 | :root => "root" 69 | } 70 | ) 71 | @file1 = "#{@tmp_path}/fake_uri.png" 72 | @file2 = "#{@tmp_path}/fake_uri_fresh.png" 73 | 2.times do 74 | @screenshot.test_screenshot(@url_w_uri) 75 | end 76 | end 77 | 78 | after(:each) do 79 | FileUtils.rm_r(@tmp_path, :force => true) 80 | end 81 | 82 | it "should create the paths_hash correctly" do 83 | ( (@screenshot.paths_hash[:original].should eq(@file1)) && (@screenshot.paths_hash[:fresh].should eq(@file2)) ).should be_true 84 | end 85 | 86 | it "should snap and save another screenshot if a screenshot already exists" do 87 | if File.exist?(@file1) 88 | File.exist?(@file2).should be_true 89 | end 90 | end 91 | 92 | it "should destroy a set of screenshots" do 93 | @screenshot.destroy(@url_w_uri) 94 | ( File.exist?(@file1) && File.exist?(@file2) ).should be_false 95 | end 96 | end 97 | 98 | describe "Custom filenaming" do 99 | 100 | after(:each) do 101 | FileUtils.rm_r(@tmp_path, :force => true) 102 | end 103 | 104 | it "should allow users to create a naming convention" do 105 | @screenshot = GreenOnion::Screenshot.new( 106 | :browser => @browser, 107 | :dir => @tmp_path, 108 | :skin_name => { 109 | :match => /[\/]/, 110 | :replace => "#", 111 | :prefix => nil, 112 | :root => "root" 113 | } 114 | ) 115 | @screenshot.get_path("#{@url}/another/uri/string") 116 | @screenshot.paths_hash[:original].should eq("#{@tmp_path}/another#uri#string.png") 117 | end 118 | 119 | it "should allow filenames to have a timestamp" do 120 | this_month = Time.now.strftime("%m_%Y_") 121 | @screenshot = GreenOnion::Screenshot.new( 122 | :browser => @browser, 123 | :dir => @tmp_path, 124 | :skin_name => { 125 | :match => /[\/]/, 126 | :replace => "-", 127 | :prefix => this_month, 128 | :root => "root" 129 | } 130 | ) 131 | @screenshot.get_path("#{@url}/another/uri/string") 132 | @screenshot.paths_hash[:original].should eq("#{@tmp_path}/#{this_month}another-uri-string.png") 133 | end 134 | 135 | it "should allow renaming for root skins" do 136 | @screenshot = GreenOnion::Screenshot.new( 137 | :browser => @browser, 138 | :dir => @tmp_path, 139 | :skin_name => { 140 | :match => /[\/]/, 141 | :replace => "-", 142 | :prefix => nil, 143 | :root => "first" 144 | } 145 | ) 146 | @screenshot.get_path(@url) 147 | @screenshot.paths_hash[:original].should eq("#{@tmp_path}/first.png") 148 | end 149 | end 150 | 151 | end -------------------------------------------------------------------------------- /spec/unit/green_onion_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe GreenOnion do 4 | 5 | before(:all) do 6 | @tmp_path = './spec/tmp' 7 | @url = 'http://localhost:8070' 8 | @url_w_uri = @url + '/fake_uri' 9 | end 10 | 11 | describe "Skins" do 12 | before(:each) do 13 | GreenOnion.configure do |c| 14 | c.skins_dir = @tmp_path 15 | end 16 | end 17 | 18 | after(:each) do 19 | FileUtils.rm_r(@tmp_path, :force => true) 20 | end 21 | 22 | it "should default to 1024x768 browser dimensions" do 23 | ( (GreenOnion.configuration.dimensions[:height] == 768) && 24 | (GreenOnion.configuration.dimensions[:width] == 1024) ).should be_true 25 | end 26 | 27 | it "should set/get custom directory" do 28 | GreenOnion.configuration.skins_dir.should eq(@tmp_path) 29 | end 30 | 31 | it "should get the correct paths_hash" do 32 | 2.times do 33 | GreenOnion.skin(@url) 34 | end 35 | ( (GreenOnion.screenshot.paths_hash[:original] == "#{@tmp_path}/root.png") && 36 | (GreenOnion.screenshot.paths_hash[:fresh] == "#{@tmp_path}/root_fresh.png") ).should be_true 37 | end 38 | 39 | it "should measure the percentage of diff between skins" do 40 | 2.times do 41 | GreenOnion.skin_percentage(@url) 42 | end 43 | GreenOnion.compare.percentage_changed.should be > 0 44 | end 45 | 46 | it "should measure the percentage of diff between skins (even if there is no diff)" do 47 | 2.times do 48 | GreenOnion.skin_percentage(@url_w_uri) 49 | end 50 | GreenOnion.compare.percentage_changed.should be == 0 51 | end 52 | 53 | it "should print just URL and changed/total when diff percentage threshold has not been surpassed" do 54 | $stdout.should_receive(:puts).exactly(3).times 55 | 2.times do 56 | GreenOnion.skin_percentage(@url, 6) 57 | end 58 | end 59 | 60 | it "should create visual diff between skins" do 61 | 2.times do 62 | GreenOnion.skin_visual(@url) 63 | end 64 | GreenOnion.compare.diffed_image.should eq("#{@tmp_path}/root_diff.png") 65 | end 66 | 67 | it "should create visual diff between skins (even when there is no change)" do 68 | 2.times do 69 | GreenOnion.skin_visual(@url_w_uri) 70 | end 71 | GreenOnion.compare.diffed_image.should eq("#{@tmp_path}/fake_uri_diff.png") 72 | end 73 | 74 | it "should measure the percentage of diff between skins AND create visual diff" do 75 | 2.times do 76 | GreenOnion.skin_visual_and_percentage(@url) 77 | end 78 | ( (GreenOnion.compare.diffed_image.should eq("#{@tmp_path}/root_diff.png")) && 79 | (GreenOnion.compare.percentage_changed.should be > 0) ).should be_true 80 | end 81 | end 82 | 83 | describe "Skins with custom dimensions" do 84 | before(:each) do 85 | GreenOnion.configure do |c| 86 | c.skins_dir = @tmp_path 87 | c.dimensions = { :width => 1440, :height => 900 } 88 | end 89 | end 90 | 91 | after(:each) do 92 | FileUtils.rm_r(@tmp_path, :force => true) 93 | end 94 | 95 | it "should allow custom browser dimensions" do 96 | ( (GreenOnion.configuration.dimensions[:height] == 900) && 97 | (GreenOnion.configuration.dimensions[:width] == 1440) ).should be_true 98 | end 99 | end 100 | 101 | describe "Skins with custom threshold" do 102 | before(:each) do 103 | GreenOnion.configure do |c| 104 | c.skins_dir = @tmp_path 105 | c.threshold = 1 106 | end 107 | end 108 | 109 | after(:each) do 110 | FileUtils.rm_r(@tmp_path, :force => true) 111 | end 112 | 113 | it "should alert when diff percentage threshold is surpassed" do 114 | GreenOnion.should_receive(:abort) 115 | 2.times do 116 | GreenOnion.skin_percentage(@url) 117 | end 118 | end 119 | end 120 | 121 | describe "Skins with custom file namespace" do 122 | 123 | after(:each) do 124 | FileUtils.rm_r(@tmp_path, :force => true) 125 | end 126 | 127 | it "should allow custom file namespacing" do 128 | GreenOnion.configure do |c| 129 | c.skins_dir = @tmp_path 130 | c.skin_name = { 131 | :match => /[\/a-z]/, 132 | :replace => "-", 133 | :prefix => "start", 134 | :root => "first" 135 | } 136 | end 137 | ( (GreenOnion.configuration.skin_name[:match] == /[\/a-z]/) && 138 | (GreenOnion.configuration.skin_name[:replace] == "-") && 139 | (GreenOnion.configuration.skin_name[:prefix] == "start") && 140 | (GreenOnion.configuration.skin_name[:root] == "first") ).should be_true 141 | end 142 | 143 | it "should allow incomplete setting of skin_name hash" do 144 | GreenOnion.configure do |c| 145 | c.skins_dir = @tmp_path 146 | c.skin_name = { 147 | :replace => "o" 148 | } 149 | end 150 | ( (GreenOnion.configuration.skin_name[:match] == /[\/]/) && 151 | (GreenOnion.configuration.skin_name[:replace] == "o") && 152 | (GreenOnion.configuration.skin_name[:prefix] == nil) && 153 | (GreenOnion.configuration.skin_name[:root] == "root") ).should be_true 154 | end 155 | end 156 | 157 | describe "Errors" do 158 | before(:each) do 159 | GreenOnion.configure do |c| 160 | c.skins_dir = @tmp_path 161 | end 162 | end 163 | 164 | after(:each) do 165 | FileUtils.rm_r(@tmp_path, :force => true) 166 | end 167 | 168 | it "should raise error for when ill-formatted URL is used" do 169 | expect { GreenOnion.skin_percentage("localhost") }.to raise_error(GreenOnion::Errors::IllformattedURL) 170 | end 171 | 172 | it "should raise error for when threshold is out of range for skin_percentage" do 173 | expect { GreenOnion.skin_percentage(@url, 101) }.to raise_error(GreenOnion::Errors::ThresholdOutOfRange) 174 | end 175 | 176 | it "should raise error for when threshold is out of range for skin_visual_and_percentage" do 177 | expect { GreenOnion.skin_visual_and_percentage(@url, 101) }.to raise_error(GreenOnion::Errors::ThresholdOutOfRange) 178 | end 179 | 180 | it "should raise error for when unknown driver is assigned" do 181 | GreenOnion.configure do |c| 182 | c.skins_dir = @tmp_path 183 | c.driver = :foo 184 | end 185 | expect { GreenOnion.skin_percentage(@url) }.to raise_error(ArgumentError) 186 | end 187 | end 188 | 189 | 190 | describe "Skins with custom driver" do 191 | before(:each) do 192 | GreenOnion.configure do |c| 193 | c.skins_dir = @tmp_path 194 | c.driver = "selenium" 195 | end 196 | end 197 | 198 | after(:each) do 199 | FileUtils.rm_r(@tmp_path, :force => true) 200 | end 201 | 202 | it "should allow custom browser driver" do 203 | GreenOnion.configuration.browser.driver.should eq("selenium") 204 | end 205 | end 206 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [](http://travis-ci.org/#!/intridea/green_onion) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/intridea/green_onion) 3 | 4 | # GreenOnion 5 | 6 | Regression issues in the view make you cry. 7 | 8 | GreenOnion is a testing library for the UI only. It alerts you when the appearance of a view has changed, let's you know the percentage of total change, and allows you to visualize the areas that have been changed. It fits right into your test suite, and is dependent on familiar tools like Capybara. 9 | 10 | ## Documentation 11 | 12 | [RDoc](http://rdoc.info/gems/green_onion/frames) 13 | 14 | ## Installation 15 | 16 | If you want to use [capybara-webkit](https://github.com/thoughtbot/capybara-webkit), you'll need to get Qt built in your testing environment. [Follow these steps](https://github.com/thoughtbot/capybara-webkit/wiki/Installing-Qt-and-compiling-capybara-webkit) and `gem install capybara-webkit` to get it up and running. Overwise, you can just use `'selenium'` (or another driver) in the configuration block. 17 | 18 | Add this line to your application's Gemfile: 19 | 20 | gem 'green_onion' 21 | 22 | And then execute: 23 | 24 | bundle 25 | 26 | Or install it yourself as: 27 | 28 | gem install green_onion 29 | 30 | ## Usage 31 | 32 | ### Command Line Interface 33 | 34 | #### Skinning 35 | 36 | To just run a comparison between skins in your shell, you can use the command below: 37 | 38 | green_onion skin [options] 39 | 40 | Options 41 | * `` - is the screen you want tested. Must include http://, example - 'http://yourSite.com' 42 | * `--dir=DIR` - the directory that GreenOnion will store all skins. The namespace for skins is {URI name}.png (original), {URI name}_fresh.png (testing), and {URI name}_diff.png. The default directory will be './spec/skins' 43 | * `--method=[p, v, vp]` - the method in which you'd like to compare the skins. `p` is for percentage, `v` is for visual. The default is visual and percentage. 44 | * `--threshold=[1-100]` is the percentage of acceptable change that the screenshots can take. This number can always be overwritten for an instance. 45 | * `--width=[number]` is the width of the browser window. The default width is 1024. (only when using capybara-webkit) 46 | * `--height=[number]` is the height of the browser window. The default height is 768. (only when using capybara-webkit) 47 | * `--browser=DRIVER` is the browser driver for Capybara. It is `webkit` by default, but you can also pass in `selenium` or `poltergeist` (for PhantomJS). 48 | 49 | #### Generating skinner file 50 | 51 | To generate a "skinner" file, which will test a Rails application with the routes without params included (this is an area that could be worked on a lot more :) ); use the command below: 52 | 53 | green_onion generate [options] 54 | 55 | * `--url=URL` - the domain that you will be testing your Rails app. The default is "http://localhost:3000". 56 | * `--dir=DIR` - the directory in which you would like to generate the skinner. The default is "spec/skinner.rb" 57 | 58 | ### Adding GreenOnion to integration tests with RSpec 59 | 60 | For adding GreenOnion to your integration tests in RSpec, add `require 'green_onion'` to your spec_helper.rb file. Place this block in the file also: 61 | 62 | GreenOnion.configure do |c| 63 | c.skins_dir = 'your/path/to/skins' 64 | c.skin_name = { 65 | :match => /[\/]/, 66 | :replace => '_', 67 | :prefix => nil, 68 | :root => 'root' 69 | } 70 | c.driver = 'webkit' 71 | c.dimensions = { 72 | :width => 1440, 73 | :height => 768 74 | } 75 | c.threshold = 20 76 | end 77 | 78 | * `skins_dir` is the directory that GreenOnion will store all skins. The namespace for skins is {URI name}.png (original), {URI name}_fresh.png (testing), and {URI name}_diff.png. The default directory will be './spec/skins' 79 | * `skin_name` is a hash that defines the skin namespace. The options include: 80 | * `:match` - a regex pattern that will replace characters from the URI. The default pattern will match to all "/" in a URI. 81 | * `:replace` - the string that replaces what is matched. These options are just abstractions of String.gsub in GreenOnion::Screenshot. 82 | * `:prefix` - a value that will be concatenated to the front of the filename. A good example would be if you wanted to add a timestamp: `:prefix => Time.now.strftime("%m_%Y_")`. 83 | * `:root` - the string that will be used to name the root of a domain. 84 | * `driver` is a string for the browser driver to use. The default is `'webkit'`. You could also pass in `'selenium'` or `'poltergeist'` (for PhantomJS) instead. 85 | * `dimensions` is a hash with the height and width of the browser window. The default dimensions are 1024x768. 86 | * `threshold` is the percentage of acceptable change that the screenshots can take. This number can always be overwritten for an instance. 87 | 88 | Then use one of the three methods below in a test... 89 | 90 | #### Percentage of change 91 | 92 | GreenOnion.skin_percentage(url, threshold [optional]) 93 | The primary feature of GreenOnion is seeing how much (if at all) a view has changed from one instance to the next, and being alerted when a view has surpassed into an unacceptable threshold. 94 | 95 | * `url` is the screen you want tested. Must include http://, example - 'http://yourSite.com' 96 | * `threshold` can be overwritten here, or if not given in the configure block – it will default to a threshold of 100% 97 | 98 | #### Viewing screenshot diffs 99 | 100 | GreenOnion.skin_visual(url) 101 | Once you are aware of a issue in the UI, you can also rip open your spec/skins directory and manually see what the differences are from one screenshot to the next. 102 | 103 | * `url` is the screen you want tested. Must include http://, example - 'http://yourSite.com' 104 | 105 | #### Both viewing screenshot diffs and percentage of change 106 | 107 | GreenOnion.skin_visual_and_percentage(url, threshold {optional}) 108 | This is just a combination of the two methods above. 109 | 110 | ## Contributing 111 | 112 | ### Testing 113 | 114 | The best way to run the specs is with... 115 | 116 | bundle exec rake spec 117 | 118 | ...this way a Sinatra WEBrick server will run concurrently with the test suite, and exit on completion. You can see the Sinatra app in spec/sample_app. 119 | 120 | ## Roadmap 121 | 122 | * Screenshots can either be viewed as a visual diff, or overlayed newest over oldest and viewed as an onion-skin with sliding transparency. 123 | * Allow for flexibility in picking browsers 124 | * Skinner generator needs love <3 125 | * Should allow for testing using fixtures/factories 126 | * More robust tests, especially around the visual diffs themselves 127 | * More documentation 128 | * More configuration/customizable settings 129 | 130 | ## THANK YOU 131 | 132 | Much of this work could not be completed without these people and projects 133 | 134 | ### [Jeff Kreeftmeijer](http://jeffkreeftmeijer.com) 135 | This is the post that got the wheels in motion: http://jeffkreeftmeijer.com/2011/comparing-images-and-creating-image-diffs/. Most of the GreenOnion::Compare class is based on this work alone. Great job Jeff! 136 | 137 | ### [Compatriot](https://github.com/carols10cents/compatriot) 138 | Carol Nichols saw the same post, and worked on an excellent gem for cross-browser testing. That gem greatly influenced design decisions with GreenOnion. 139 | 140 | ### [VCR](https://github.com/myronmarston/vcr) 141 | Many patterns and ideas also came from VCR, because of its flexibility in allowing users to pick what gems to work with. 142 | 143 | ### [Capybara](https://github.com/jnicklas/capybara), [ChunkyPNG](https://github.com/wvanbergen/chunky_png), [Thor](https://github.com/wycats/thor), and [OilyPNG](https://github.com/wvanbergen/oily_png) 144 | The land on which we sow our bulbs. 145 | 146 | ## Contributor 147 | [Ted O'Meara](http://www.intridea.com/about/team/ted-o-meara) 148 | 149 | ## License 150 | MIT License. See LICENSE for details. 151 | 152 | ## Copyright 153 | Copyright (c) 2012 Intridea, Inc. --------------------------------------------------------------------------------