zucchini-ios 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +17 -1
  2. data/CHANGELOG.md +28 -15
  3. data/README.md +23 -19
  4. data/Rakefile +1 -0
  5. data/bin/zucchini +4 -11
  6. data/lib/zucchini.rb +12 -0
  7. data/lib/{approver.rb → zucchini/approver.rb} +0 -0
  8. data/lib/{config.rb → zucchini/config.rb} +0 -0
  9. data/lib/{detector.rb → zucchini/detector.rb} +0 -0
  10. data/lib/{feature.rb → zucchini/feature.rb} +25 -22
  11. data/lib/{generator.rb → zucchini/generator.rb} +5 -5
  12. data/lib/{report.rb → zucchini/report.rb} +3 -4
  13. data/lib/zucchini/report/css/zucchini.report.css +241 -0
  14. data/lib/{report → zucchini/report}/js/jquery.effects.core.js +0 -0
  15. data/lib/{report → zucchini/report}/js/jquery.js +0 -0
  16. data/lib/{report → zucchini/report}/js/jquery.ui.core.js +0 -0
  17. data/lib/zucchini/report/js/zucchini.report.js +59 -0
  18. data/lib/zucchini/report/template.erb.html +47 -0
  19. data/lib/{report → zucchini/report}/view.rb +0 -0
  20. data/lib/{runner.rb → zucchini/runner.rb} +0 -0
  21. data/lib/{screenshot.rb → zucchini/screenshot.rb} +77 -30
  22. data/lib/{uia → zucchini/uia}/base.coffee +0 -1
  23. data/lib/{uia → zucchini/uia}/screen.coffee +5 -4
  24. data/lib/{version.rb → zucchini/version.rb} +1 -1
  25. data/spec/lib/{config_spec.rb → zucchini/config_spec.rb} +0 -0
  26. data/spec/lib/{detector_spec.rb → zucchini/detector_spec.rb} +0 -0
  27. data/spec/lib/{feature_spec.rb → zucchini/feature_spec.rb} +0 -0
  28. data/spec/lib/{generator_spec.rb → zucchini/generator_spec.rb} +0 -0
  29. data/spec/lib/{report_spec.rb → zucchini/report_spec.rb} +0 -0
  30. data/spec/lib/zucchini/screenshot_spec.rb +164 -0
  31. data/spec/sample_setup/feature_one/run_data/Run 1/06_sign up_spinner.png b/data/spec/sample_setup/feature_one/run_data/Run 1/06_splash-screen_sign → up_spinner.png +0 -0
  32. data/spec/sample_setup/support/masks/splash.png +0 -0
  33. data/spec/spec_helper.rb +14 -10
  34. data/zucchini-ios.gemspec +8 -9
  35. metadata +58 -36
  36. data/lib/report/css/zucchini.report.css +0 -239
  37. data/lib/report/js/zucchini.report.js +0 -59
  38. data/lib/report/template.erb +0 -47
  39. data/spec/lib/screenshot_spec.rb +0 -109
@@ -0,0 +1,59 @@
1
+ var ci = function() { return $(document.body).hasClass('ci') }
2
+
3
+ var scrollSurface = function(surface)
4
+ {
5
+ if(ci())
6
+ {
7
+ setTimeout(function()
8
+ {
9
+ surface.animate
10
+ (
11
+ { left: surface.offset().left - surface.parents('.viewport').width() - 26 },
12
+ 1000, 'easeInOutQuint',
13
+ function()
14
+ {
15
+ if(ci())
16
+ {
17
+ if($('.screen', surface).last().offset().left <= 27)
18
+ {
19
+ setTimeout(function()
20
+ {
21
+ surface.parents('.feature').fadeOut(500, function()
22
+ {
23
+ surface[0].style.left = 0
24
+ var next = nextSurface(surface)
25
+ next.parents('.feature').fadeIn(500, function(){ scrollSurface(next) })
26
+ })
27
+ }, 2000)
28
+ }
29
+ else scrollSurface(surface)
30
+ }
31
+ }
32
+ )
33
+ }, 2000)
34
+ }
35
+ }
36
+
37
+ var nextSurface = function(surface)
38
+ {
39
+ var next = $('.surface', surface.parents('.feature').next())
40
+ return !next.length ? $('.surface').first() : next
41
+ }
42
+
43
+ var bindEvents = function(callback)
44
+ {
45
+ $('.screen').each(function(){ $(this).click(function()
46
+ {
47
+ if(ci())
48
+ $(document.body).switchClass('ci', '', 100, function() { $('.feature').css({ display: 'block' }) })
49
+ else
50
+ $(this).toggleClass('expanded')
51
+ }) })
52
+
53
+ $('.buttons .expand' ).click(function(){ $('.screen', $(this).parents('.feature')).addClass( 'expanded') })
54
+ $('.buttons .collapse').click(function(){ $('.screen', $(this).parents('.feature')).removeClass('expanded') })
55
+
56
+ callback()
57
+ }
58
+
59
+ $(document).ready(function() { bindEvents(function() { scrollSurface($('.surface').first()) }) })
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
3
+
4
+ <head>
5
+ <title>Test Results: <%= @device[:name] %>, <%= @time %></title>
6
+ <link rel="stylesheet" href="<%= @assets_path %>/css/zucchini.report.css" type="text/css" media="screen">
7
+ <script src="<%= @assets_path %>/js/jquery.js"></script>
8
+ <script src="<%= @assets_path %>/js/jquery.ui.core.js"></script>
9
+ <script src="<%= @assets_path %>/js/jquery.effects.core.js"></script>
10
+ <script src="<%= @assets_path %>/js/zucchini.report.js"></script>
11
+ </head>
12
+ <body class="<%= @ci %>">
13
+ <header>
14
+ <h1><%= @device[:name] %><span class="time"><%= @time %></span></h1>
15
+ </header>
16
+
17
+ <% @features.each_with_index do |f, i| %>
18
+ <section class="<%= i == 0 ? 'first' : '' %> feature">
19
+ <h3><%= f.name %></h3>
20
+ <div class="indicators">
21
+ <% if !f.stats[:failed].empty? %><div class="failed"><%= f.stats[:failed].length.to_s %></div><% end %>
22
+ <% if !f.stats[:pending].empty? %><div class="pending"><%= f.stats[:pending].length.to_s %></div><% end %>
23
+ <% if !f.stats[:passed].empty? %><div class="passed"><%= f.stats[:passed].length.to_s %></div><% end %>
24
+ </div>
25
+
26
+ <div class="buttons">
27
+ <a class="expand left">Expand</a>
28
+ <a class="collapse right">Collapse</a>
29
+ </div>
30
+
31
+ <div class="viewport">
32
+ <div class="surface">
33
+ <% f.screenshots.each do |s| %>
34
+ <% css_class = s.diff[0].to_s %>
35
+ <dl class="<%= css_class %> <%= css_class == 'failed' ? 'expanded' :'' %> screen">
36
+ <dt><%= s.file_name %></dt>
37
+ <% %W(actual expected difference).each do |type| %>
38
+ <dd class="<%= s.result_images[type.to_sym] ? '' : 'hidden' %>"><p><%= type.capitalize %></p><img src="<%= s.result_images[type.to_sym] %>"/></dd>
39
+ <% end %>
40
+ </dl>
41
+ <% end %>
42
+ </div>
43
+ </div>
44
+ </section>
45
+ <% end %>
46
+ </body>
47
+ </html>
File without changes
File without changes
@@ -1,57 +1,67 @@
1
1
  class Zucchini::Screenshot
2
- ORIENTATION = /^\d\d_(?<orientation>(Unknown|Portrait|PortraitUpsideDown|LandscapeLeft|LandscapeRight|FaceUp|FaceDown))_.*$/
2
+ FILE_NAME_PATTERN = /^\d\d_((?<orientation>Unknown|Portrait|PortraitUpsideDown|LandscapeLeft|LandscapeRight|FaceUp|FaceDown)_)?((?<screen>.*)-screen_)?.*$/
3
3
 
4
- attr_reader :file_path, :file_name
4
+ attr_reader :file_path, :original_file_path, :file_name
5
5
  attr_accessor :diff, :masks_paths, :masked_paths, :test_path, :diff_path
6
6
 
7
7
  def initialize(file_path, device, unmatched_pending = false)
8
- file_name = File.basename(file_path)
9
8
  @original_file_path = file_path
10
- @device = device
11
-
12
- @orientation = (match = ORIENTATION.match(file_name)) ? match[:orientation] : nil
13
- if @orientation
14
- @file_name = file_name.gsub("_#{@orientation}", '')
15
- @file_path = File.join(File.dirname(file_path),@file_name)
16
- else
17
- @file_name = file_name
18
- @file_path = file_path
9
+ @file_path = file_path.dup
10
+
11
+ @device = device
12
+
13
+ match = FILE_NAME_PATTERN.match(File.basename(@file_path))
14
+
15
+ if match
16
+ @orientation = match[:orientation]
17
+ @screen = match[:screen]
18
+ @file_path.gsub!("_#{@screen}-screen", '') if @screen
19
+ @file_path.gsub!("_#{@orientation}", '') if @orientation
19
20
  end
20
21
 
22
+ @file_name = File.basename(@file_path)
23
+
21
24
  unless unmatched_pending
22
25
  file_base_path = File.dirname(@file_path)
23
26
 
27
+ support_masks_path = "#{file_base_path}/../../../support/masks"
28
+
24
29
  @masks_paths = {
25
- :global => "#{file_base_path}/../../../support/masks/#{@device[:screen]}.png",
30
+ :global => "#{support_masks_path}/#{@device[:screen]}.png",
31
+ :screen => "#{support_masks_path}/#{@screen.to_s.underscore}.png",
26
32
  :specific => "#{file_base_path}/../../masks/#{@device[:screen]}/#{@file_name}"
27
33
  }
28
34
 
29
35
  masked_path = "#{file_base_path}/../Masked/actual/#{@file_name}"
30
- @masked_paths = { :globally => masked_path, :specifically => masked_path }
36
+ @masked_paths = { :global => masked_path, :screen => masked_path, :specific => masked_path }
31
37
 
32
38
  @diff_path = "#{file_base_path}/../Diff/#{@file_name}"
33
39
  end
40
+
41
+ preprocess
34
42
  end
35
43
 
36
- def rotate
37
- return unless @orientation
38
- degrees = case @orientation
39
- when 'LandscapeRight' then 90
40
- when 'LandscapeLeft' then 270
41
- when 'PortraitUpsideDown' then 180
44
+ def preprocess
45
+ return if @original_file_path == @file_path
46
+
47
+ if @orientation
48
+ rotate
42
49
  else
43
- 0
50
+ FileUtils.rm @file_path if File.exists?(@file_path)
51
+ FileUtils.mv @original_file_path, @file_path
44
52
  end
45
- `convert \"#{@original_file_path}\" -rotate \"#{degrees}\" \"#{@file_path}\"`
46
- FileUtils.rm @original_file_path
47
53
  end
48
54
 
49
55
  def mask
50
- @masked_paths.each { |name, path| FileUtils.mkdir_p(File.dirname(path)) }
51
- `convert -page +0+0 \"#{@file_path}\" -page +0+0 \"#{@masks_paths[:global]}\" -flatten \"#{@masked_paths[:globally]}\"`
56
+ create_masked_paths_dirs
57
+ masked_path = apply_mask(@file_path, :global)
58
+
59
+ if mask?(:screen)
60
+ masked_path = apply_mask(masked_path, :screen)
61
+ end
52
62
 
53
- if File.exists?(@masks_paths[:specific])
54
- `convert -page +0+0 \"#{@masked_paths[:globally]}\" -page +0+0 \"#{@masks_paths[:specific]}\" -flatten \"#{@masked_paths[:specifically]}\"`
63
+ if mask?(:specific)
64
+ apply_mask(masked_path, :specific)
55
65
  end
56
66
  end
57
67
 
@@ -62,7 +72,7 @@ class Zucchini::Screenshot
62
72
  FileUtils.mkdir_p(File.dirname(@diff_path))
63
73
 
64
74
  compare_command = "compare -metric AE -fuzz 2% -dissimilarity-threshold 1 -subimage-search"
65
- out = `#{compare_command} \"#{@masked_paths[:specifically]}\" \"#{@test_path}\" \"#{@diff_path}\" 2>&1`
75
+ out = `#{compare_command} \"#{@masked_paths[:specific]}\" \"#{@test_path}\" \"#{@diff_path}\" 2>&1`
66
76
  out.chomp!
67
77
  @diff = (out == '0') ? [:passed, nil] : [:failed, out]
68
78
  @diff = [:pending, @diff[1]] if @pending
@@ -73,7 +83,7 @@ class Zucchini::Screenshot
73
83
 
74
84
  def result_images
75
85
  @result_images ||= {
76
- :actual => @masked_paths && File.exists?(@masked_paths[:specifically]) ? @masked_paths[:specifically] : nil,
86
+ :actual => @masked_paths && File.exists?(@masked_paths[:specific]) ? @masked_paths[:specific] : nil,
77
87
  :expected => @test_path && File.exists?(@test_path) ? @test_path : nil,
78
88
  :difference => @diff_path && File.exists?(@diff_path) ? @diff_path : nil
79
89
  }
@@ -92,10 +102,47 @@ class Zucchini::Screenshot
92
102
 
93
103
  reference = Zucchini::Screenshot.new(reference_file_path, @device)
94
104
  reference.masks_paths = @masks_paths
95
- reference.masked_paths = { :globally => output_path, :specifically => output_path }
105
+ reference.masked_paths = { :global => output_path, :screen => output_path, :specific => output_path }
96
106
  reference.mask
97
107
  end
98
108
  end
99
109
  end
100
110
 
111
+ private
112
+ def mask?(mask)
113
+ @masks_paths[mask] && File.exists?(@masks_paths[mask])
114
+ end
115
+
116
+ def create_masked_paths_dirs
117
+ @masked_paths.each { |name, path| FileUtils.mkdir_p(File.dirname(path)) }
118
+ end
119
+
120
+ def apply_mask(src_path, mask)
121
+ mask_path = @masks_paths[mask]
122
+ dest_path = @masked_paths[mask]
123
+ `convert -page +0+0 \"#{src_path}\" -page +0+0 \"#{mask_path}\" -flatten \"#{dest_path}\"`
124
+ return dest_path
125
+ end
126
+
127
+ def rotate
128
+ degrees = case @orientation
129
+ when 'LandscapeRight' then 90
130
+ when 'LandscapeLeft' then 270
131
+ when 'PortraitUpsideDown' then 180
132
+ else
133
+ 0
134
+ end
135
+ `convert \"#{@original_file_path}\" -rotate \"#{degrees}\" \"#{@file_path}\"`
136
+ FileUtils.rm @original_file_path
137
+ end
138
+ end
139
+
140
+ class String
141
+ def underscore
142
+ self.gsub(/::/, '/').
143
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
144
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
145
+ tr("-", "_").
146
+ downcase
147
+ end
101
148
  end
@@ -76,7 +76,6 @@ class Zucchini
76
76
 
77
77
  if found
78
78
  puts "Found anchor for screen '#{screenName}'"
79
- target.delay 1
80
79
  else
81
80
  raise "Could not find anchor for screen '#{screenName}'"
82
81
 
@@ -1,5 +1,5 @@
1
1
  class Screen
2
- takeScreenshot: (name) ->
2
+ takeScreenshot: (screenshot_name) ->
3
3
  orientation = switch app.interfaceOrientation()
4
4
  when 0 then 'Unknown'
5
5
  when 1 then 'Portrait'
@@ -8,7 +8,8 @@ class Screen
8
8
  when 4 then 'LandscapeRight'
9
9
  when 5 then 'FaceUp'
10
10
  when 6 then 'FaceDown'
11
- target.captureScreenWithName("#{orientation}_#{name}")
11
+ puts "Screenshot of screen '#{@name}' taken"
12
+ target.captureScreenWithName("#{orientation}_#{@name}-screen_#{screenshot_name}")
12
13
 
13
14
  constructor: (@name) ->
14
15
 
@@ -17,8 +18,8 @@ class Screen
17
18
  'Take a screenshot$' : ->
18
19
  @takeScreenshot(@name)
19
20
 
20
- 'Take a screenshot named "([^"]*)"$' : (name) ->
21
- @takeScreenshot(name)
21
+ 'Take a screenshot named "([^"]*)"$' : (screenshot_name) ->
22
+ @takeScreenshot(screenshot_name)
22
23
 
23
24
  'Tap "([^"]*)"$' : (element) ->
24
25
  raise "Element '#{element}' not defined for the screen '#{@name}'" unless @elements[element]
@@ -1,3 +1,3 @@
1
1
  module Zucchini
2
- VERSION = "0.6.1"
2
+ VERSION = "0.6.2"
3
3
  end
@@ -0,0 +1,164 @@
1
+ require 'spec_helper'
2
+ require 'digest/md5'
3
+ require 'tmpdir'
4
+
5
+ def md5(blob)
6
+ Digest::MD5.hexdigest(blob)
7
+ end
8
+
9
+ describe Zucchini::Screenshot do
10
+ describe "general" do
11
+ let (:device) { { :name => "iPhone 4S", :screen => "retina_ios5", :udid => "rspec987654" } }
12
+ let (:screen) { "splash" }
13
+ let (:base_path) { "spec/sample_setup/feature_one" }
14
+ let (:original_path) { "#{base_path}/run_data/Run\ 1/06_splash-screen_sign\ up_spinner.png" }
15
+
16
+ before do
17
+ @screenshot = Zucchini::Screenshot.new(original_path, device)
18
+ @screenshot.masked_paths = {
19
+ :global => "#{base_path}/global_masked.png",
20
+ :screen => "#{base_path}/screen_masked.png",
21
+ :specific => "#{base_path}/specific_masked.png"
22
+ }
23
+ end
24
+
25
+ after do
26
+ FileUtils.mv(@screenshot.file_path, original_path) if File.exists?(@screenshot.file_path)
27
+
28
+ @screenshot.masked_paths.each do |k, path|
29
+ FileUtils.rm(path) if File.exists?(path)
30
+ end
31
+
32
+ FileUtils.rm_rf("#{base_path}/run_data/Masked")
33
+ FileUtils.rm_rf(@screenshot.diff_path)
34
+ end
35
+
36
+ describe "mask" do
37
+ let (:checksums) {
38
+ checksums = {}
39
+
40
+ checksums[:original] = md5(File.read(@screenshot.file_path))
41
+
42
+ if File.exists?(@screenshot.masked_paths[:global])
43
+ checksums[:global_masked] = md5(File.read(@screenshot.masked_paths[:global]))
44
+ end
45
+
46
+ if File.exists?(@screenshot.masked_paths[:screen])
47
+ checksums[:screen_masked] = md5(File.read(@screenshot.masked_paths[:screen]))
48
+ end
49
+
50
+ if File.exists?(@screenshot.masked_paths[:specific])
51
+ checksums[:specific_masked] = md5(File.read(@screenshot.masked_paths[:specific]))
52
+ end
53
+
54
+ checksums
55
+ }
56
+
57
+ it "should apply a standard global mask based on the device" do
58
+ @screenshot.mask
59
+ File.exists?(@screenshot.masked_paths[:global]).should be true
60
+ checksums[:global_masked].should_not be_equal checksums[:original]
61
+ end
62
+
63
+ it "should apply a screen-specific mask if it exists" do
64
+ @screenshot.mask
65
+ File.exists?(@screenshot.masked_paths[:screen]).should be true
66
+ checksums[:screen_masked].should_not be_equal checksums[:original]
67
+ checksums[:specific_masked].should_not be_equal checksums[:global_masked]
68
+ end
69
+
70
+ it "should not apply a screen mask if it does not exist" do
71
+ @screenshot.masks_paths[:screen] = nil
72
+ @screenshot.mask
73
+ File.exists?(@screenshot.masked_paths[:screen]).should_not be true
74
+ checksums[:global_masked].should_not be_equal checksums[:original]
75
+ end
76
+
77
+ it "should not apply a specific mask if it does not exist" do
78
+ @screenshot.masks_paths[:specific] = nil
79
+ @screenshot.mask
80
+ File.exists?(@screenshot.masked_paths[:specific]).should_not be true
81
+ checksums[:global_masked].should_not be_equal checksums[:original]
82
+ end
83
+
84
+ it "should apply a screenshot-specific mask if it exists" do
85
+ @screenshot.mask
86
+ File.exists?(@screenshot.masked_paths[:specific]).should be true
87
+ checksums[:specific_masked].should_not be_equal checksums[:original]
88
+ checksums[:specific_masked].should_not be_equal checksums[:screen_masked]
89
+ checksums[:specific_masked].should_not be_equal checksums[:global_masked]
90
+ end
91
+ end
92
+
93
+ describe "compare" do
94
+ context "images are identical" do
95
+ it "should have a passed indicator in the diff" do
96
+ @screenshot.mask
97
+ @screenshot.compare
98
+ @screenshot.diff.should eq [:passed, nil]
99
+ end
100
+ end
101
+
102
+ context "images are different" do
103
+ it "should have a failed indicator in the diff" do
104
+ @screenshot.stub!(:mask_reference)
105
+ @screenshot.test_path = "#{base_path}/reference/#{device[:screen]}/06_sign\ up_spinner_error.png"
106
+ @screenshot.mask
107
+ @screenshot.compare
108
+ @screenshot.diff.should eq [:failed, "188162"]
109
+ end
110
+
111
+ it "should have a failed indicator in the diff with no screen mask" do
112
+ @screenshot.stub!(:mask_reference)
113
+ @screenshot.test_path = "#{base_path}/reference/#{device[:screen]}/06_sign\ up_spinner_error.png"
114
+ @screenshot.masks_paths[:screen] = nil
115
+ @screenshot.mask
116
+ @screenshot.compare
117
+ @screenshot.diff.should eq [:failed, "3017"]
118
+ end
119
+ end
120
+ end
121
+
122
+ describe "mask reference" do
123
+ it "should create masked versions of reference screenshots" do
124
+ @screenshot.mask
125
+ @screenshot.mask_reference
126
+
127
+ File.exists?(@screenshot.test_path).should be_true
128
+ md5(File.read(@screenshot.test_path)).should_not be_equal md5(File.read("#{base_path}/reference/#{device[:screen]}/06_sign\ up_spinner.png"))
129
+ end
130
+ end
131
+ end
132
+
133
+ describe "rotate" do
134
+ let (:sample_screenshots_path) { "spec/sample_screenshots" }
135
+ let (:reference_screenshot_path) { File.join(sample_screenshots_path, "rotated/01_Screenshot.png") }
136
+ let (:reference_md5) { md5(File.read(reference_screenshot_path)) }
137
+ let (:temp_dir) { Dir.mktmpdir }
138
+ before do
139
+ `cp #{File.join(sample_screenshots_path, "*")} #{temp_dir}`
140
+ end
141
+
142
+ it "should rotate the landscape-left screenshot" do
143
+ screenshot = Zucchini::Screenshot.new(File.join(temp_dir,'01_LandscapeLeft_Screenshot.png'), nil, true)
144
+ File.exists?(File.join(temp_dir,'01_LandscapeLeft_Screenshot.png')).should be_false
145
+ File.exists?(File.join(temp_dir,'01_Screenshot.png')).should be_true
146
+ end
147
+
148
+ it "should rotate the landscape-right screenshot" do
149
+ screenshot = Zucchini::Screenshot.new(File.join(temp_dir,'02_LandscapeRight_Screenshot.png'), nil, true)
150
+ File.exists?(File.join(temp_dir,'02_LandscapeRight_Screenshot.png')).should be_false
151
+ File.exists?(File.join(temp_dir,'02_Screenshot.png')).should be_true
152
+ end
153
+
154
+ it "should rotate the upside-down screenshot" do
155
+ screenshot = Zucchini::Screenshot.new(File.join(temp_dir,'03_PortraitUpsideDown_Screenshot.png'), nil, true)
156
+ File.exists?(File.join(temp_dir,'02_PortraitUpsideDown_Screenshot.png')).should be_false
157
+ File.exists?(File.join(temp_dir,'03_Screenshot.png')).should be_true
158
+ end
159
+
160
+ after do
161
+ FileUtils.remove_entry temp_dir
162
+ end
163
+ end
164
+ end