compatriot 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Version 0.0.3
2
+
3
+ Release date: 2011-12-20
4
+
5
+ ### Fixed
6
+
7
+ * General messiness
8
+ * Issue #1 - Test failure on OSX: due to not actually deleting files between test runs
9
+ * Issue #2 - Opens over 9000 browsers during tests: now only one integration test opens browsers.
10
+
11
+ ### Changed
12
+
13
+ * Lots of structure in response to unit test pain; clear up responsibilities somewhat.
14
+
15
+ ### Added
16
+
17
+ * Unit tests!
18
+
19
+ # Version 0.0.2
20
+
21
+ Release date: 2011-12-02
22
+
23
+ ### Fixed
24
+
25
+ * Low release number: no one uses v0.0.1.
26
+ * My knowledge of the `rake release` task (it creates a git tag for you)
27
+
28
+ # Version 0.0.1
29
+
30
+ Release date: 2011-12-01
31
+
32
+ ### Added
33
+
34
+ * Initial proof of concept
data/README.md CHANGED
@@ -1,25 +1,28 @@
1
1
  Compatriot
2
- ----------
3
- **Compat**ibility + **riot**! It's your **friend** that helps you with browser compatibility!
2
+ ==========
4
3
 
5
- What this is
6
- ------------
4
+ **Compat**ibility + **riot**! It's the **friend** that helps with browser compatibility!
5
+ This Ruby gem makes cross-browser testing less painful.
6
+ Its goal is to help identify pages that appear to have significant variations when rendered in different browsers.
7
+ You can spend your time fixing the cross-browser problems rather than looking for them.
8
+ Don't let your users find the inconsistencies and get to them first.
7
9
 
8
- This is a Ruby gem to make cross-browser testing less painful. Its goal is to help identify pages that appear to have significant variations when rendered in different browsers. That way, you can spend your time fixing the cross-browser incompatibilities rather than spending your time looking for them, or worse, not looking for them at all and letting your users find them.
9
10
 
10
11
  What it does now
11
12
  ----------------
12
13
 
13
- * Goes to the root path of a Rack app in firefox and chrome and takes a screenshot in each.
14
- * Stores the screenshot in tmp/results/_timestamp_/_browser_/
15
- * Creates tmp/results/_timestamp_/index.html that shows thumbnails of each screenshot plus a diff of the two in a table for easy comparison.
14
+ * In firefox and chrome, visits a list of paths to a Rack app and takes a screenshot on each page.
15
+ * Stores the screenshot in `tmp/results/_timestamp_/_browser_/`
16
+ * Creates `tmp/results/_timestamp_/index.html` that shows thumbnails of each screenshot plus a diff of the two in a table for easy comparison.
17
+
16
18
 
17
19
  What it will do in the future
18
20
  -----------------------------
21
+
19
22
  * Have documentation
20
23
  * Have a screenshot of sample results in the README
21
24
  * Have unit tests and better tests
22
- * Be on travisci
25
+ * Be on travis-ci
23
26
  * Not have a diff that's a different size than the originals
24
27
  * Find the largest, darkest contiguous region in the image diff and have a threshold of pass/fail based on that
25
28
  * Perform better on the image processing (by sampling/resizing, using oily_png, etc)
@@ -29,21 +32,22 @@ What it will do in the future
29
32
  * Allow configuration of which browsers to use
30
33
  * Connect to virtual machines so that you don't have to have all the browsers on the machine you're running the tests on
31
34
 
32
- What to do to use it
33
- --------------------
34
35
 
35
- ### Firefox
36
- * Need to have firefox installed
36
+ How To Use
37
+ ----------
38
+
39
+ **Requirements**
40
+
41
+ * Ruby v1.9.2
42
+ * [Firefox](http://getfirefox.net)
43
+ * [chromedriver](http://code.google.com/p/selenium/wiki/ChromeDriver)
37
44
 
38
- ### Chrome
39
- * Need chromedriver in your path
40
- * From: http://code.google.com/p/chromium/downloads/list
41
- * More info: http://code.google.com/p/selenium/wiki/ChromeDriver
45
+ When you run a file similar to the examples it will save results in `_current-directory_/tmp/results/_timestamp_/_browser_`
42
46
 
43
- See examples in the examples directory. When you run a file similar to the examples, it will save results in _current-directory_/tmp/results/_timestamp_/_browser_
44
47
 
45
48
  What to do to run its tests
46
49
  ---------------------------
50
+
47
51
  Using at least ruby 1.9.2:
48
52
 
49
53
  bundle install
@@ -51,14 +55,25 @@ Using at least ruby 1.9.2:
51
55
 
52
56
  This is using minispec for testing.
53
57
 
54
- What to do to contribute
55
- ------------------------
56
58
 
57
- Contributions are very welcome! This is a very rough proof-of-concept at this point, so there are many opportunities for improvement. Feel free to:
59
+ How You Can Contribute
60
+ ----------------------
61
+
62
+ * [Issues](https://github.com/clnclarinet/compatriot/issues)
63
+
64
+ We'd really like to know if something is wrong, so please file an issue on the Issue List if you have a problem, suggestion, unsupported use case, etc.
65
+
66
+ This is a very rough proof-of-concept at this point, so there are many opportunities for improvement. Feel free to:
58
67
 
59
- * File a github issue with any problems or suggestions <3
60
- * Fork and send a pull request with failing tests illustrating the bug or currently unsupported use case <3 <3
61
- * Fork and send a pull request with bugfixes or new features with passing tests! <3 <3 <3
68
+ * **Fork** the repository
69
+ * **Clone the repository** locally, or **edit via Github**
70
+ * Create a **new branch** with a meaningful name of the issue or feature you're working on
71
+ * Commit **often** and **when important**
72
+ * **Write tests** specifically for the changes you've made (unless you're fixing a failing test. Also: just submitting new tests for untested code is a big help too!)
73
+ * **Push** your feature or bug fix branch to your Github fork
74
+ * Make a **Pull Request** from your fork to the main repo
75
+
76
+ Ryan Bates did an awesome [Railscast on contributing to open source](http://railscasts.com/episodes/300-contributing-to-open-source) that walks through this process, but please let us know if you have any questions or problems.
62
77
 
63
78
  Standing on the shoulders of giants
64
79
  -----------------------------------
@@ -69,12 +84,16 @@ Many thanks to the wonderful libraries that make this gem possible:
69
84
  * [selenium-webdriver](http://seleniumhq.org/docs/01_introducing_selenium.html#selenium-2-aka-selenium-webdriver)
70
85
  * [chunky_png](https://github.com/wvanbergen/chunky_png) (and especially [this blog post about using chunky_png to create image diffs](http://jeffkreeftmeijer.com/2011/comparing-images-and-creating-image-diffs/?utm_source=rubyweekly&utm_medium=email) by Jeff Kreeftmeijer)
71
86
 
87
+
72
88
  Contributors
73
89
  ------------
74
90
  * Carol Nichols ([twitter](http://twitter.com/carols10cents), [website](http://carol-nichols.com))
75
91
  * Andrew Cox ([twitter](https://twitter.com/coxandrew), [website](http://andrewcox.org/))
92
+ * Kurtis Rainbolt-Greene ([twitter](https://twitter.com/krainboltgreene)) ([website](http://kurtisrainboltgreene.name/))
93
+ * Steve Klabnik ([twitter](https://twitter.com/steveklabnik)) ([website](http://www.steveklabnik.com/))
76
94
  * You???
77
95
 
96
+
78
97
  License
79
98
  -------
80
99
 
@@ -97,4 +116,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
97
116
  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
98
117
  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
99
118
  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
100
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
119
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile CHANGED
@@ -9,3 +9,8 @@ task :test do
9
9
  end
10
10
  task("tests").execute
11
11
  end
12
+
13
+ desc "Run tests"
14
+ task :spec do
15
+ task("test").execute
16
+ end
data/compatriot.gemspec CHANGED
@@ -4,8 +4,12 @@ require File.expand_path('../lib/compatriot/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Carol Nichols"]
6
6
  gem.email = ["carol.nichols@gmail.com"]
7
- gem.description = %q{Finds likely UI browser cross-compatibility issues.}
8
- gem.summary = %q{Runs a command in multiple browsers using selenium then compares the screenshots and presents those likely to have cross-browser incompatibilities.}
7
+ gem.description = 'Finds likely UI browser cross-compatibility issues.'
8
+ gem.summary = %q{
9
+ Runs a command in multiple browsers using selenium then compares the
10
+ screenshots and presents those likely to have cross-browser incompatibilities.
11
+ }
12
+
9
13
  gem.homepage = "https://github.com/clnclarinet/compatriot"
10
14
 
11
15
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -15,9 +19,12 @@ Gem::Specification.new do |gem|
15
19
  gem.require_paths = ["lib"]
16
20
  gem.version = Compatriot::VERSION
17
21
 
18
- gem.add_runtime_dependency "capybara", "~> 1.1.2"
19
- gem.add_runtime_dependency "rake", "~> 0.9.2.2"
20
- gem.add_runtime_dependency "chunky_png", "~> 1.2.5"
22
+ # To get the newest minitest features!
23
+ gem.add_development_dependency 'minitest', '~> 2.8.1'
24
+ gem.add_development_dependency 'sinatra', '~> 1.3.1'
25
+ gem.add_development_dependency 'mocha', '~> 0.10.0'
21
26
 
22
- gem.add_development_dependency "sinatra", "~> 1.3.1"
27
+ gem.add_dependency 'capybara', '~> 1.1.2'
28
+ gem.add_dependency 'rake', '~> 0.9.2.2'
29
+ gem.add_dependency 'chunky_png', '~> 1.2.5'
23
30
  end
@@ -5,52 +5,78 @@ module Compatriot
5
5
  class Browser
6
6
  include Capybara::DSL
7
7
 
8
- def initialize(browser_name, results_directory)
9
- @results_directory = results_directory
10
- @browser_name = browser_name
11
- @browser_selenium_id = translate_to_selenium(@browser_name)
12
- @file_id = 1
8
+ def self.create_browsers(params = {})
9
+ params[:browser_names].collect do |b|
10
+ Compatriot::Browser.new(
11
+ :name => b,
12
+ :screenshot_directory => params[:results_directory]
13
+ )
14
+ end
15
+ end
16
+
17
+ attr_reader :name
18
+
19
+ def initialize(params = {})
20
+ @name = params[:name]
21
+ @screenshot_directory = params[:screenshot_directory]
22
+ @screenshot_locations = {}
23
+
24
+ create_screenshot_path
25
+ end
26
+
27
+ def screenshot_path
28
+ File.join(@screenshot_directory, @name)
13
29
  end
14
30
 
15
31
  def initialize_capybara(app)
16
- driver = "selenium_#{@browser_name}".to_sym
32
+ driver = "selenium_#{@name}".to_sym
17
33
  Capybara.register_driver driver do |a|
18
- Capybara::Selenium::Driver.new(a, :browser => @browser_selenium_id)
34
+ Capybara::Selenium::Driver.new(a, :browser => @name.to_sym)
19
35
  end
20
36
  Capybara.default_driver = driver
21
37
  Capybara.app = app
22
38
  end
23
39
 
24
- def take_screenshots(paths)
25
- results = {}
26
- paths.each do |path|
27
- visit path
28
- results[path] = screenshot
29
- end
40
+ def take_screenshots(params = {})
41
+ initialize_capybara(params[:app])
42
+ params[:paths].map { |path| take_screenshot(path) }
43
+
30
44
  # Reset the selenium session to avoid timeout errors
31
45
  Capybara.send(:session_pool).delete_if { |key, value| key =~ /selenium/i }
32
- results
33
46
  end
34
47
 
35
- def screenshot
36
- file_base_name = "#{@file_id}.png"
37
- @file_id += 1
38
- filepath = File.join(screenshot_path, file_base_name)
39
- Capybara.page.driver.browser.save_screenshot(filepath)
40
- file_base_name
48
+ def absolute_screenshot_for(path)
49
+ if @screenshot_locations[path]
50
+ absolute_dir(@screenshot_locations[path])
51
+ end
41
52
  end
42
53
 
43
- def screenshot_path
44
- return @screenshot_path if @screenshot_path
45
- @screenshot_path = "#{@results_directory}/#{@browser_name}"
46
- FileUtils.mkdir_p(@screenshot_path)
47
- @screenshot_path
54
+ def relative_screenshot_for(path)
55
+ if @screenshot_locations[path]
56
+ File.join(name, @screenshot_locations[path])
57
+ end
58
+ end
59
+
60
+ def take_screenshot(path)
61
+ visit path
62
+ filename = next_filename
63
+ Capybara.page.driver.browser.save_screenshot(absolute_dir(filename))
64
+ @screenshot_locations[path] = filename
48
65
  end
49
66
 
50
67
  private
51
68
 
52
- def translate_to_selenium(browser_name)
53
- browser_name.to_sym
69
+ def absolute_dir(filename)
70
+ File.join(screenshot_path, filename)
71
+ end
72
+
73
+ def next_filename
74
+ @file_id = (@file_id && @file_id + 1) || 1
75
+ "#{@file_id}.png"
76
+ end
77
+
78
+ def create_screenshot_path
79
+ FileUtils.mkdir_p(screenshot_path)
54
80
  end
55
81
  end
56
82
  end
@@ -4,64 +4,97 @@ include ChunkyPNG::Color
4
4
  module Compatriot
5
5
  class ImageDiffer
6
6
 
7
- def self.diff(results)
7
+ attr_reader :diffs
8
+
9
+ def initialize(params = {})
10
+ @paths = params[:paths]
11
+ @browsers = params[:browsers]
12
+ @strategy = params[:strategy] || :color_difference
13
+ @diffs = {}
14
+ end
15
+
16
+ def compute!
17
+ @paths.map do |path|
18
+ images_to_diff = @browsers.map { |b| b.absolute_screenshot_for(path) }
19
+ @diffs[path] = diff(images_to_diff)
20
+ end
21
+ end
22
+
23
+ def diff(results)
8
24
  images = results.map{|r| ChunkyPNG::Image.from_file(r) }
9
- f = self.color_difference(images, results.first)
10
- File.join(
11
- File.basename(File.dirname(f)),
12
- File.basename(f)
13
- )
25
+ self.send(@strategy, images.first, images.last, results.first)
14
26
  end
15
27
 
16
- def self.same_pixels_exactly(images, name)
17
- output = ChunkyPNG::Image.new(images.first.width, images.last.width, WHITE)
28
+ def same_pixels_exactly(image1, image2, name)
29
+ output = ChunkyPNG::Image.new(image1.width, image2.height, WHITE)
18
30
  diff = []
19
31
 
20
- images.first.height.times do |y|
21
- images.first.row(y).each_with_index do |pixel, x|
22
- output[x,y] = pixel
23
- diff << [x,y] unless pixel == images.last[x,y]
24
- end
32
+ each_pixel(image1) do |x, y|
33
+ pixel1 = image1[x, y]
34
+ pixel2 = image2[x, y]
35
+ output[x,y] = pixel1
36
+ diff << [x,y] unless pixel1 == pixel2
25
37
  end
26
38
 
27
- pixels_total = images.first.pixels.length
39
+ pixels_total = image1.pixels.length
28
40
  pixels_changed = diff.length
29
- pixels_changed_percentage = (diff.length.to_f / images.first.pixels.length) * 100
41
+ pixels_changed_percentage = (diff.length.to_f / image1.pixels.length) * 100
30
42
 
31
43
  x, y = diff.map{ |xy| xy[0] }, diff.map{ |xy| xy[1] }
32
44
 
33
45
  output.rect(x.min, y.min, x.max, y.max, ChunkyPNG::Color.rgb(0,255,0))
34
46
  filename = "#{name}-same_exactly.png"
35
47
  output.save(filename)
36
- filename
48
+ File.join(
49
+ File.basename(File.dirname(filename)),
50
+ File.basename(filename)
51
+ )
37
52
  end
38
53
 
39
- def self.color_difference(images, name)
40
- output = ChunkyPNG::Image.new(images.first.width, images.last.width, WHITE)
54
+ def color_difference(image1, image2, name)
55
+ output = ChunkyPNG::Image.new(image1.width, image1.height, WHITE)
41
56
  diff = []
42
57
 
43
- images.first.height.times do |y|
44
- images.first.row(y).each_with_index do |pixel, x|
45
- unless pixel == images.last[x,y]
46
- score = Math.sqrt(
47
- (r(images.last[x,y]) - r(pixel)) ** 2 +
48
- (g(images.last[x,y]) - g(pixel)) ** 2 +
49
- (b(images.last[x,y]) - b(pixel)) ** 2
50
- ) / Math.sqrt(MAX ** 2 * 3)
51
-
52
- output[x,y] = grayscale(MAX - (score * MAX).round)
53
- diff << score
54
- end
58
+ each_pixel(image1) do |x, y|
59
+ pixel1 = image1[x,y]
60
+ pixel2 = image2[x,y]
61
+ unless pixel1 == pixel2
62
+ output[x,y], score = color_difference_of_pixels(pixel1, pixel2)
63
+ diff << score
55
64
  end
56
65
  end
57
66
 
58
- pixels_total = images.first.pixels.length
59
- pixels_changed = diff.length
60
- pixels_changed_percentage = (diff.inject {|sum, value| sum + value} / images.first.pixels.length) * 100
61
-
62
67
  filename = "#{name}-color_difference.png"
63
68
  output.save(filename)
64
- filename
69
+ File.join(
70
+ File.basename(File.dirname(filename)),
71
+ File.basename(filename)
72
+ )
73
+ end
74
+
75
+ def color_difference_of_pixels(pixel1, pixel2)
76
+ score = Math.sqrt(
77
+ (r(pixel2) - r(pixel1)) ** 2 +
78
+ (g(pixel2) - g(pixel1)) ** 2 +
79
+ (b(pixel2) - b(pixel1)) ** 2
80
+ ) / Math.sqrt(MAX ** 2 * 3)
81
+
82
+ [grayscale(MAX - (score * MAX).round), score]
83
+ end
84
+
85
+ # Not called anywhere
86
+ def color_difference_total_score
87
+ pixels_total = image1.width * image1.height
88
+ pixels_changed = diff.length
89
+ pixels_changed_percentage = (diff.inject {|sum, value| sum + value} / pixels_total) * 100
90
+ end
91
+
92
+ def each_pixel(image)
93
+ image.width.times do |x|
94
+ image.height.times do |y|
95
+ yield(x, y)
96
+ end
97
+ end
65
98
  end
66
99
  end
67
100
  end
@@ -6,12 +6,13 @@ module Compatriot
6
6
  @results_directory = results_directory
7
7
  end
8
8
 
9
- def make_index_page(results)
9
+ def make_index_page(params = {})
10
10
  index_page_file = File.join(@results_directory, "index.html")
11
11
 
12
- b = binding
13
- browsers = results.browsers
14
- paths = results.paths
12
+ b = binding
13
+ paths = params[:paths]
14
+ browsers = params[:browsers]
15
+ differ = params[:differ]
15
16
 
16
17
  html = ERB.new <<-EOF
17
18
  <html>
@@ -32,7 +33,7 @@ module Compatriot
32
33
  <tr>
33
34
  <th>Path</th>
34
35
  <% browsers.each do |browser| %>
35
- <th><%= browser %></th>
36
+ <th><%= browser.name %></th>
36
37
  <% end %>
37
38
  <th>Diff</th>
38
39
  </tr>
@@ -43,11 +44,11 @@ module Compatriot
43
44
  </td>
44
45
  <% browsers.each do |browser| %>
45
46
  <td>
46
- <img src="<%= browser %>/<%= results.screenshot_for(browser, path) %>" />
47
+ <img src="<%= browser.relative_screenshot_for(path) %>" />
47
48
  </td>
48
49
  <% end %>
49
50
  <td>
50
- <img src="<%= results.diff_for(path) %>" />
51
+ <img src="<%= differ.diffs[path] %>" />
51
52
  </td>
52
53
  </tr>
53
54
  <% end %>
@@ -4,6 +4,7 @@ module Compatriot
4
4
  class Runner
5
5
  def self.start(app, paths, clock = DateTime)
6
6
  runner = new(app, paths, clock)
7
+ runner.create_results_directory
7
8
  runner.take_screenshots
8
9
  runner.compute_diffs
9
10
  runner.make_index_page
@@ -12,41 +13,51 @@ module Compatriot
12
13
 
13
14
  BROWSERS = ["firefox", "chrome"]
14
15
 
15
- attr_reader :app, :results
16
+ attr_reader :app, :results_directory
16
17
 
17
18
  def initialize(app, paths, clock)
18
19
  @app = app
19
20
  @paths = paths
20
21
  @clock = clock
21
- @results = Compatriot::Results.new
22
+
23
+ timestamp = @clock.now.strftime("%Y-%m-%d-%H-%M-%S")
24
+ @results_directory = File.join("tmp", "results", timestamp)
25
+
26
+ @browsers = Compatriot::Browser.create_browsers(
27
+ :browser_names => BROWSERS,
28
+ :results_directory => @results_directory
29
+ )
22
30
  end
23
31
 
24
32
  def take_screenshots
25
- BROWSERS.each do |b|
26
- @results.take_screenshots(
27
- :browser => b,
28
- :app => @app,
29
- :paths => @paths,
30
- :results_directory => results_directory
33
+ @browsers.each do |browser_object|
34
+ browser_object.take_screenshots(
35
+ :app => @app,
36
+ :paths => @paths
31
37
  )
32
38
  end
33
39
  end
34
40
 
35
41
  def compute_diffs
36
- @results.compute_diffs
42
+ @differ = Compatriot::ImageDiffer.new(
43
+ :paths => @paths,
44
+ :browsers => @browsers,
45
+ :strategy => :color_difference
46
+ )
47
+ @differ.compute!
37
48
  end
38
49
 
39
50
  def make_index_page
40
- presenter = Compatriot::ResultsPresenter.new(results_directory)
41
- presenter.make_index_page(@results)
51
+ presenter = Compatriot::ResultsPresenter.new(@results_directory)
52
+ presenter.make_index_page(
53
+ :paths => @paths,
54
+ :browsers => @browsers,
55
+ :differ => @differ
56
+ )
42
57
  end
43
58
 
44
- def results_directory
45
- return @results_directory if @results_directory
46
- timestamp = @clock.now.strftime("%Y-%m-%d-%H-%M-%S")
47
- directory_name = "tmp/results/#{timestamp}"
48
- FileUtils.mkdir_p(directory_name)
49
- @results_directory = directory_name
59
+ def create_results_directory
60
+ FileUtils.mkdir_p(@results_directory)
50
61
  end
51
62
  end
52
63
  end
@@ -1,3 +1,3 @@
1
1
  module Compatriot
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -0,0 +1,36 @@
1
+ require_relative '../spec_helper'
2
+ require 'fileutils'
3
+ require 'date'
4
+ require 'nokogiri'
5
+
6
+ describe "Hit a list of paths for this app" do
7
+ it "takes screenshots, diffs them, and creates an index" do
8
+ root_dir = Dir.getwd
9
+ Dir.chdir(File.join(File.dirname(__FILE__), '..', 'sample_app'))
10
+
11
+ results_tmp_dir = File.join("tmp", "results")
12
+
13
+ FileUtils.remove_dir(results_tmp_dir, true)
14
+
15
+ Compatriot.app = TestApp
16
+ Compatriot.run(%w[
17
+ /
18
+ /chrome-css-bug
19
+ ])
20
+
21
+ current_results_dir = (Dir.entries(results_tmp_dir) - [".", ".."]).first
22
+ current_results_dir = File.join(results_tmp_dir, current_results_dir)
23
+
24
+ firefox_dir = File.join(current_results_dir, "firefox")
25
+ chrome_dir = File.join(current_results_dir, "chrome")
26
+
27
+ Dir.glob(File.join(firefox_dir, "*.png")).size.must_equal 4
28
+ Dir.glob(File.join(chrome_dir, "*.png")).size.must_equal 2
29
+
30
+ results_index = IO.read(File.join(current_results_dir, "index.html"))
31
+ xml = Nokogiri::XML(results_index)
32
+ xml.xpath("//tr[td]").size.must_equal(2)
33
+
34
+ Dir.chdir(root_dir)
35
+ end
36
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,51 @@
1
1
  $:.unshift(File.expand_path('../lib', File.dirname(__FILE__)))
2
2
 
3
+ require 'rubygems'
4
+ gem 'minitest' # ensures you're using the gem, and not the built in MT
3
5
  require 'minitest/autorun'
6
+ require 'mocha'
7
+
4
8
  require 'compatriot'
5
9
 
6
10
  require_relative "sample_app/test_app"
7
11
 
8
- FileUtils.remove_dir(File.join("sample_app", "tmp", "results"), true)
12
+ # A custom runner to enable before_suite and after_suite setup/teardown.
13
+ # http://bfts.rubyforge.org/minitest/index.html
14
+ # Only using it to delete the screenshots that result from running the tests
15
+ # before running the suite; it's useful to be able to look at the screenshots
16
+ # after a test run so we're not deleting them then.
17
+
18
+ module MiniTestWithHooks
19
+ class Unit < MiniTest::Unit
20
+ def before_suites
21
+ end
22
+
23
+ def after_suites
24
+ end
25
+
26
+ def _run_suites(suites, type)
27
+ begin
28
+ before_suites
29
+ super(suites, type)
30
+ ensure
31
+ after_suites
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ module MiniTestRemoveScreenshots
38
+ class Unit < MiniTestWithHooks::Unit
39
+
40
+ def before_suites
41
+ super
42
+ FileUtils.remove_dir(File.join("sample_app", "tmp", "results"), true)
43
+ end
44
+
45
+ def after_suites
46
+ super
47
+ end
48
+ end
49
+ end
50
+
51
+ MiniTest::Unit.runner = MiniTestRemoveScreenshots::Unit.new
@@ -0,0 +1,167 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Compatriot::Browser do
4
+ describe "self#create_browsers" do
5
+ it "creates a browser object for each browser name given to it" do
6
+ browsers = ["linx", "dolphin HD"] # Selenium totally doesn't work w/these
7
+ fake_directory = "/some/location"
8
+
9
+ linx = stub
10
+ dolphin = stub
11
+
12
+ Compatriot::Browser.expects(:new).with(
13
+ :name => "linx",
14
+ :screenshot_directory => fake_directory
15
+ ).returns(linx)
16
+
17
+ Compatriot::Browser.expects(:new).with(
18
+ :name => "dolphin HD",
19
+ :screenshot_directory => fake_directory
20
+ ).returns(dolphin)
21
+
22
+ results = Compatriot::Browser.create_browsers(
23
+ :browser_names => browsers,
24
+ :results_directory => fake_directory
25
+ )
26
+ results.must_equal([linx, dolphin])
27
+ end
28
+ end
29
+
30
+ describe "initialize_capybara" do
31
+ before do
32
+ Compatriot::Browser.any_instance.stubs(:create_screenshot_path)
33
+ @b = Compatriot::Browser.new(
34
+ :name => "foo",
35
+ :screenshot_directory => "bar"
36
+ )
37
+ end
38
+
39
+ it "registers a new driver" do
40
+ Capybara.expects(:register_driver).with(:selenium_foo)
41
+
42
+ @b.initialize_capybara(stub)
43
+ end
44
+
45
+ it "sets this driver as the default" do
46
+ Capybara.expects(:default_driver=).with(:selenium_foo)
47
+
48
+ @b.initialize_capybara(stub)
49
+ end
50
+
51
+ it "sets the Capybara app" do
52
+ app = stub
53
+ Capybara.expects(:app=).with(app)
54
+
55
+ @b.initialize_capybara(app)
56
+ end
57
+ end
58
+
59
+ describe "take_screenshots" do
60
+ before do
61
+ Compatriot::Browser.any_instance.stubs(:create_screenshot_path)
62
+ @b = Compatriot::Browser.new(
63
+ :name => "foo",
64
+ :screenshot_directory => "bar"
65
+ )
66
+ end
67
+
68
+ it "calls initialize_capybara once and take_screenshot for each path" do
69
+ app = stub
70
+ @b.expects(:initialize_capybara).with(app)
71
+
72
+ @b.expects(:take_screenshot).with("/")
73
+ @b.expects(:take_screenshot).with("/contact")
74
+
75
+ @b.take_screenshots(
76
+ :app => app,
77
+ :paths => ["/", "/contact"]
78
+ )
79
+ end
80
+ end
81
+
82
+ describe "take_screenshot" do
83
+ before do
84
+ Compatriot::Browser.any_instance.stubs(:create_screenshot_path)
85
+ @b = Compatriot::Browser.new(
86
+ :name => "foo",
87
+ :screenshot_directory => "bar"
88
+ )
89
+ end
90
+
91
+ it "visits the path" do
92
+ @b.expects(:visit).with("/some_page")
93
+
94
+ capybara_browser = stub
95
+ capybara_browser.stubs(:save_screenshot)
96
+ Capybara.page.driver.stubs(:browser).returns(capybara_browser)
97
+
98
+ @b.take_screenshot("/some_page")
99
+ end
100
+
101
+ it "tells capybara to take a screenshot" do
102
+ @b.stubs(:visit)
103
+
104
+ @b.stubs(:next_filename).returns("/where_to_save")
105
+ capybara_browser = stub
106
+ capybara_browser.expects(:save_screenshot).with("bar/foo/where_to_save")
107
+ Capybara.page.driver.stubs(:browser).returns(capybara_browser)
108
+
109
+ @b.take_screenshot("/some_page")
110
+ end
111
+
112
+ it "increments the filenames" do
113
+ @b.stubs(:visit)
114
+
115
+ capybara_browser = stub
116
+ capybara_browser.stubs(:save_screenshot)
117
+ Capybara.page.driver.stubs(:browser).returns(capybara_browser)
118
+
119
+ @b.take_screenshot("/some_page")
120
+ @b.take_screenshot("/some_other_page")
121
+
122
+ @b.relative_screenshot_for("/some_page").must_equal("foo/1.png")
123
+ @b.relative_screenshot_for("/some_other_page").must_equal("foo/2.png")
124
+ end
125
+
126
+ end
127
+
128
+ describe "relative_screenshot_for" do
129
+ before do
130
+ Compatriot::Browser.any_instance.stubs(:create_screenshot_path)
131
+ @b = Compatriot::Browser.new(
132
+ :name => "foo",
133
+ :screenshot_directory => "bar"
134
+ )
135
+ end
136
+
137
+ it "returns nil if there is no screenshot for that path" do
138
+ @b.relative_screenshot_for("/whatever").must_equal(nil)
139
+ end
140
+
141
+ it "stores the screenshot location by path" do
142
+ @b.stubs(:visit)
143
+ @b.stubs(:next_filename).returns("/some/location.png")
144
+
145
+ capybara_browser = stub
146
+ capybara_browser.stubs(:save_screenshot)
147
+ Capybara.page.driver.stubs(:browser).returns(capybara_browser)
148
+
149
+ @b.take_screenshot("/")
150
+
151
+ @b.relative_screenshot_for("/").must_equal("foo/some/location.png")
152
+ end
153
+ end
154
+
155
+ describe "screenshot_path" do
156
+ it "adds the browser name to the screenshot dir and creates it" do
157
+ FileUtils.expects(:mkdir_p).with("foo/bar/hi")
158
+
159
+ c = Compatriot::Browser.new(
160
+ :name => "hi",
161
+ :screenshot_directory => "foo/bar"
162
+ )
163
+
164
+ c.screenshot_path.must_equal("foo/bar/hi")
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,53 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Compatriot::ImageDiffer do
4
+ describe "compute!" do
5
+ it "diffs each set of images and stores the location by path" do
6
+ d = Compatriot::ImageDiffer.new(
7
+ :paths => ["/home"],
8
+ :browsers => [
9
+ stub(:absolute_screenshot_for => "1.png"),
10
+ stub(:absolute_screenshot_for => "2.png")
11
+ ]
12
+ )
13
+ d.expects(:diff).with(["1.png", "2.png"]).returns("diff.png")
14
+ d.compute!
15
+ d.diffs["/home"].must_equal("diff.png")
16
+ end
17
+ end
18
+ describe "diff" do
19
+ it "calls chunky_png on each image path" do
20
+ file_one = stub
21
+ file_two = stub
22
+
23
+ ChunkyPNG::Image.expects(:from_file).with(file_one)
24
+ ChunkyPNG::Image.expects(:from_file).with(file_two)
25
+
26
+ c = Compatriot::ImageDiffer.new
27
+ c.stubs(:color_difference)
28
+ c.diff([file_one, file_two])
29
+ end
30
+
31
+ it "returns the filename of the diff" do
32
+ end
33
+
34
+ it "uses the strategy passed in" do
35
+ end
36
+ end
37
+
38
+ describe "self#color_difference" do
39
+ it "starts a new white image with the same dimensions" do
40
+ diff = stub_everything
41
+ ChunkyPNG::Image.expects(:new).with(
42
+ 1,
43
+ 2,
44
+ ChunkyPNG::Image::WHITE
45
+ ).returns(diff)
46
+
47
+ image1 = stub_everything("1", :width => 1, :height => 2)
48
+ image2 = stub_everything("2", :width => 3, :height => 4)
49
+
50
+ Compatriot::ImageDiffer.new.color_difference(image1, image2, stub)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,64 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Compatriot::Runner do
4
+ before do
5
+ Compatriot::Runner.any_instance.stubs(:create_results_directory)
6
+ Compatriot::Browser.any_instance.stubs(:create_screenshot_path)
7
+ fake_date = DateTime.parse("2012-01-01 00:00:00 UTC")
8
+ @fake_date_dir = fake_date.strftime("%Y-%m-%d-%H-%M-%S")
9
+ @fixed_clock = OpenStruct.new(:now => fake_date)
10
+ @results_dir_name = File.join("tmp", "results", @fake_date_dir)
11
+ end
12
+
13
+ describe "self#start" do
14
+ it "creates a Runner instance and calls the other methods on it" do
15
+ runner = stub
16
+
17
+ Compatriot::Runner.expects(:new).returns(runner)
18
+ runner.expects(:create_results_directory)
19
+ runner.expects(:take_screenshots)
20
+ runner.expects(:compute_diffs)
21
+ runner.expects(:make_index_page)
22
+
23
+ Compatriot::Runner.start(stub, stub)
24
+ end
25
+ end
26
+
27
+ describe "#take_screenshots" do
28
+ it "calls take_screenshots for each browser" do
29
+ firefox_browser = stub
30
+ chrome_browser = stub
31
+
32
+ Compatriot::Browser.expects(:create_browsers).returns([
33
+ firefox_browser,
34
+ chrome_browser
35
+ ])
36
+
37
+ app = stub
38
+ paths = stub
39
+
40
+ firefox_browser.expects(:take_screenshots).with(
41
+ :app => app,
42
+ :paths => paths
43
+ )
44
+ chrome_browser.expects(:take_screenshots).with(
45
+ :app => app,
46
+ :paths => paths
47
+ )
48
+
49
+ runner = Compatriot::Runner.new(app, paths, @fixed_clock)
50
+
51
+ runner.take_screenshots
52
+ end
53
+ end
54
+
55
+
56
+ describe "#results_directory" do
57
+ it "names a results directory in tmp/results based on the clock" do
58
+ runner = Compatriot::Runner.new(TestApp, ["/"], @fixed_clock)
59
+
60
+ runner.results_directory.must_equal(@results_dir_name)
61
+ end
62
+ end
63
+
64
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compatriot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,44 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-02 00:00:00.000000000Z
12
+ date: 2011-12-21 00:00:00.000000000Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minitest
16
+ requirement: &2157131520 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.8.1
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *2157131520
25
+ - !ruby/object:Gem::Dependency
26
+ name: sinatra
27
+ requirement: &2157131020 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 1.3.1
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2157131020
36
+ - !ruby/object:Gem::Dependency
37
+ name: mocha
38
+ requirement: &2157130560 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 0.10.0
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2157130560
14
47
  - !ruby/object:Gem::Dependency
15
48
  name: capybara
16
- requirement: &2153232360 !ruby/object:Gem::Requirement
49
+ requirement: &2157130100 !ruby/object:Gem::Requirement
17
50
  none: false
18
51
  requirements:
19
52
  - - ~>
@@ -21,10 +54,10 @@ dependencies:
21
54
  version: 1.1.2
22
55
  type: :runtime
23
56
  prerelease: false
24
- version_requirements: *2153232360
57
+ version_requirements: *2157130100
25
58
  - !ruby/object:Gem::Dependency
26
59
  name: rake
27
- requirement: &2153231860 !ruby/object:Gem::Requirement
60
+ requirement: &2157129640 !ruby/object:Gem::Requirement
28
61
  none: false
29
62
  requirements:
30
63
  - - ~>
@@ -32,10 +65,10 @@ dependencies:
32
65
  version: 0.9.2.2
33
66
  type: :runtime
34
67
  prerelease: false
35
- version_requirements: *2153231860
68
+ version_requirements: *2157129640
36
69
  - !ruby/object:Gem::Dependency
37
70
  name: chunky_png
38
- requirement: &2153231400 !ruby/object:Gem::Requirement
71
+ requirement: &2157129180 !ruby/object:Gem::Requirement
39
72
  none: false
40
73
  requirements:
41
74
  - - ~>
@@ -43,18 +76,7 @@ dependencies:
43
76
  version: 1.2.5
44
77
  type: :runtime
45
78
  prerelease: false
46
- version_requirements: *2153231400
47
- - !ruby/object:Gem::Dependency
48
- name: sinatra
49
- requirement: &2153230940 !ruby/object:Gem::Requirement
50
- none: false
51
- requirements:
52
- - - ~>
53
- - !ruby/object:Gem::Version
54
- version: 1.3.1
55
- type: :development
56
- prerelease: false
57
- version_requirements: *2153230940
79
+ version_requirements: *2157129180
58
80
  description: Finds likely UI browser cross-compatibility issues.
59
81
  email:
60
82
  - carol.nichols@gmail.com
@@ -64,6 +86,7 @@ extra_rdoc_files: []
64
86
  files:
65
87
  - .gitignore
66
88
  - .rvmrc
89
+ - CHANGELOG.md
67
90
  - Gemfile
68
91
  - README.md
69
92
  - Rakefile
@@ -72,15 +95,16 @@ files:
72
95
  - lib/compatriot.rb
73
96
  - lib/compatriot/browser.rb
74
97
  - lib/compatriot/image_differ.rb
75
- - lib/compatriot/results.rb
76
98
  - lib/compatriot/results_presenter.rb
77
99
  - lib/compatriot/runner.rb
78
100
  - lib/compatriot/version.rb
79
- - spec/basic_behavior_spec.rb
80
- - spec/list_of_app_paths_spec.rb
101
+ - spec/integration/full_stack_spec.rb
81
102
  - spec/sample_app/public/images/smileyface.jpg
82
103
  - spec/sample_app/test_app.rb
83
104
  - spec/spec_helper.rb
105
+ - spec/unit/browser_spec.rb
106
+ - spec/unit/image_differ_spec.rb
107
+ - spec/unit/runner_spec.rb
84
108
  homepage: https://github.com/clnclarinet/compatriot
85
109
  licenses: []
86
110
  post_install_message:
@@ -107,8 +131,10 @@ specification_version: 3
107
131
  summary: Runs a command in multiple browsers using selenium then compares the screenshots
108
132
  and presents those likely to have cross-browser incompatibilities.
109
133
  test_files:
110
- - spec/basic_behavior_spec.rb
111
- - spec/list_of_app_paths_spec.rb
134
+ - spec/integration/full_stack_spec.rb
112
135
  - spec/sample_app/public/images/smileyface.jpg
113
136
  - spec/sample_app/test_app.rb
114
137
  - spec/spec_helper.rb
138
+ - spec/unit/browser_spec.rb
139
+ - spec/unit/image_differ_spec.rb
140
+ - spec/unit/runner_spec.rb
@@ -1,43 +0,0 @@
1
- module Compatriot
2
- class Results
3
- def initialize
4
- @data = {}
5
- @diffs = {}
6
- end
7
-
8
- def take_screenshots(params)
9
- @results_directory = params[:results_directory]
10
- @app = params[:app]
11
- @paths = params[:paths]
12
-
13
- @browser = Compatriot::Browser.new(params[:browser], @results_directory)
14
- @browser.initialize_capybara(@app)
15
-
16
- @data[params[:browser]] = @browser.take_screenshots(@paths)
17
- end
18
-
19
- def compute_diffs
20
- paths.map do |path|
21
- @diffs[path] = Compatriot::ImageDiffer.diff(
22
- browsers.map{|b| File.join(@results_directory, b, screenshot_for(b, path))}
23
- )
24
- end
25
- end
26
-
27
- def browsers
28
- @data.keys
29
- end
30
-
31
- def paths
32
- @data[browsers.first].keys
33
- end
34
-
35
- def screenshot_for(browser, path)
36
- @data[browser][path]
37
- end
38
-
39
- def diff_for(path)
40
- @diffs[path]
41
- end
42
- end
43
- end
@@ -1,53 +0,0 @@
1
- require_relative 'spec_helper'
2
- require 'fileutils'
3
- require 'date'
4
-
5
- describe "The very basics" do
6
- before do
7
- @current_dir = Dir.getwd
8
- Dir.chdir(File.join(File.dirname(__FILE__), 'sample_app'))
9
-
10
- fake_date = DateTime.parse("2012-01-01 00:00:00 UTC")
11
- fake_date_dir = fake_date.strftime("%Y-%m-%d-%H-%M-%S")
12
- @fixed_clock = OpenStruct.new(:now => fake_date)
13
-
14
- @results_directory = File.join("tmp", "results", fake_date_dir)
15
- @firefox_directory = File.join(@results_directory, "firefox")
16
- @chrome_directory = File.join(@results_directory, "chrome")
17
- end
18
-
19
- after do
20
- Dir.chdir(@current_dir)
21
- end
22
-
23
- it "holds onto the app we give it" do
24
- x = Compatriot::Runner.new(TestApp, ["/"], @fixed_clock)
25
- x.app.must_equal(TestApp)
26
- end
27
-
28
- it "names a results directory based on the clock" do
29
- x = Compatriot::Runner.new(TestApp, ["/"], @fixed_clock)
30
- x.results_directory.must_equal(@results_directory)
31
- end
32
-
33
- describe "self#start" do
34
- before do
35
- Compatriot::Runner.start(TestApp, ["/"], @fixed_clock)
36
- end
37
-
38
- it "creates directories in which to store the results based on the date" do
39
- assert File.exists?(@results_directory)
40
- assert File.exists?(@firefox_directory)
41
- assert File.exists?(@chrome_directory)
42
- end
43
-
44
- it "visits the home page and takes 1 screenshot per browser and diffs them" do
45
- Dir.glob(File.join(@firefox_directory, "*.png")).size.must_equal 2
46
- Dir.glob(File.join(@chrome_directory, "*.png")).size.must_equal 1
47
- end
48
-
49
- it "creates an index page that shows the screenshots" do
50
- assert File.exists?(File.join(@results_directory, "index.html"))
51
- end
52
- end
53
- end
@@ -1,55 +0,0 @@
1
- require_relative 'spec_helper'
2
- require 'fileutils'
3
- require 'date'
4
- require 'nokogiri'
5
-
6
- describe "Hit a list of paths for this app" do
7
- before do
8
- @current_dir = Dir.getwd
9
- Dir.chdir(File.join(File.dirname(__FILE__), 'sample_app'))
10
-
11
- fake_date = DateTime.parse("2012-01-02 00:00:00 UTC")
12
- fake_date_dir = fake_date.strftime("%Y-%m-%d-%H-%M-%S")
13
- @fixed_clock = OpenStruct.new(:now => fake_date)
14
-
15
- @results_directory = File.join("tmp", "results", fake_date_dir)
16
- @firefox_directory = File.join(@results_directory, "firefox")
17
- @chrome_directory = File.join(@results_directory, "chrome")
18
-
19
- @x = Compatriot::Runner.start(TestApp, ["/", "/chrome-css-bug"], @fixed_clock)
20
- end
21
-
22
- after do
23
- Dir.chdir(@current_dir)
24
- end
25
-
26
- it "takes a screenshot for each path given and diffs them" do
27
- Dir.glob(File.join(@firefox_directory, "*.png")).size.must_equal 6
28
- Dir.glob(File.join(@chrome_directory, "*.png")).size.must_equal 2
29
- end
30
-
31
- it "has 2 rows of results in the table on the results index" do
32
- results_index = IO.read(File.join(@results_directory, "index.html"))
33
- xml = Nokogiri::XML(results_index)
34
- xml.xpath("//tr[td]").size.must_equal(2)
35
- end
36
-
37
- it "gets a list of the browsers in the results" do
38
- @x.results.browsers.must_equal(["firefox", "chrome"])
39
- end
40
-
41
- it "gets a list of the paths in the results" do
42
- @x.results.paths.must_equal(["/", "/chrome-css-bug"])
43
- end
44
-
45
- it "gets the filename of the screenshot for a particular browser and path" do
46
- @x.results.screenshot_for("firefox", "/").must_equal("1.png")
47
- @x.results.screenshot_for("firefox", "/chrome-css-bug").must_equal("2.png")
48
- @x.results.screenshot_for("chrome", "/").must_equal("1.png")
49
- @x.results.screenshot_for("chrome", "/chrome-css-bug").must_equal("2.png")
50
- end
51
-
52
- it "should have the home page row colored green and the chrome bug row colored red" do
53
- @x.compute_diffs
54
- end
55
- end