zucchini-ios 0.7.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/CHANGELOG.md +13 -0
  2. data/README.md +40 -17
  3. data/lib/zucchini.rb +1 -0
  4. data/lib/zucchini/approver.rb +2 -2
  5. data/lib/zucchini/compiler.rb +3 -1
  6. data/lib/zucchini/feature.rb +19 -10
  7. data/lib/zucchini/log.rb +59 -0
  8. data/lib/zucchini/report.rb +14 -31
  9. data/lib/zucchini/reporters/html.rb +75 -0
  10. data/lib/zucchini/{report → reporters/html}/css/zucchini.report.css +0 -0
  11. data/lib/zucchini/{report → reporters/html}/js/jquery.effects.core.js +0 -0
  12. data/lib/zucchini/{report → reporters/html}/js/jquery.js +0 -0
  13. data/lib/zucchini/{report → reporters/html}/js/jquery.ui.core.js +0 -0
  14. data/lib/zucchini/{report → reporters/html}/js/zucchini.report.js +0 -0
  15. data/lib/zucchini/{report → reporters/html}/template.erb.html +4 -1
  16. data/lib/zucchini/reporters/tap.rb +30 -0
  17. data/lib/zucchini/runner.rb +3 -2
  18. data/lib/zucchini/screenshot.rb +59 -48
  19. data/lib/zucchini/uia/screen.coffee +2 -2
  20. data/lib/zucchini/version.rb +1 -1
  21. data/templates/feature/feature.zucchini +5 -6
  22. data/templates/feature/masks/{retina_ios5 → retina_ios6}/.gitkeep +0 -0
  23. data/templates/feature/pending/{retina_ios5 → retina_ios6}/.gitkeep +0 -0
  24. data/templates/feature/reference/{retina_ios5 → retina_ios6}/.gitkeep +0 -0
  25. data/templates/project/features/support/config.yml +8 -7
  26. data/templates/project/features/support/lib/helpers.coffee +1 -1
  27. data/templates/project/features/support/masks/{ipad_ios5.png → ipad_ios6.png} +0 -0
  28. data/templates/project/features/support/masks/{retina_ios5.png → retina_ios6.png} +0 -0
  29. data/templates/project/features/support/screens/welcome.coffee +5 -5
  30. data/zucchini-ios.gemspec +3 -2
  31. metadata +34 -19
  32. data/lib/zucchini/report/view.rb +0 -16
  33. data/templates/project/features/support/masks/low_ios4.png +0 -0
@@ -1,3 +1,12 @@
1
+ ## 0.7.2 / 2013-08-19
2
+ * Implement orientation specific masks - [@phatmann][], [#34][]
3
+ * Archivable HTML reports - [@vaskas][], [#33][]
4
+ * TAP-compatible output and reporting - [@vaskas][], [#32][]
5
+
6
+ ## 0.7.1 / 2013-08-12
7
+ * Compile config file with ERB - [@vaskas][], [#30][]
8
+ * Reduce gem size by not bundling specs with their sample setups
9
+
1
10
  ## 0.7.0 / 2013-08-11
2
11
  * Integrate mechanic.js - [@vaskas][], [#29][]
3
12
  * Configure simulator with device and orientation - [@phatmann][], [#27][]
@@ -59,6 +68,10 @@
59
68
  [#26]: https://github.com/zucchini-src/zucchini/issues/26
60
69
  [#27]: https://github.com/zucchini-src/zucchini/issues/27
61
70
  [#29]: https://github.com/zucchini-src/zucchini/issues/29
71
+ [#30]: https://github.com/zucchini-src/zucchini/issues/30
72
+ [#32]: https://github.com/zucchini-src/zucchini/issues/32
73
+ [#33]: https://github.com/zucchini-src/zucchini/issues/33
74
+ [#34]: https://github.com/zucchini-src/zucchini/issues/34
62
75
 
63
76
  [@Jaco-Pretorius]: https://github.com/Jaco-Pretorius
64
77
  [@NathanSudell]: https://github.com/NathanSudell
data/README.md CHANGED
@@ -25,21 +25,21 @@ gem install zucchini-ios
25
25
  Using Zucchini doesn't involve making any modifications to your application code.
26
26
  You might as well keep your Zucchini tests in a separate project.
27
27
 
28
- Start by creating a project scaffold:
28
+ To create a project scaffold:
29
29
 
30
30
  ```
31
31
  zucchini generate --project /path/to/my_project
32
32
  ```
33
33
 
34
- Create a feature scaffold for your first feature:
34
+ Then to create a feature scaffold for your first feature:
35
35
 
36
36
  ```
37
37
  zucchini generate --feature /path/to/my_project/features/my_feature
38
38
  ```
39
39
 
40
- Start hacking by modifying `features/my_feature/feature.zucchini` and `features/support/screens/welcome.coffee`.
40
+ Start developing by editing `features/my_feature/feature.zucchini` and `features/support/screens/welcome.coffee`.
41
41
 
42
- Alternatively, check out the [zucchini-demo](https://github.com/zucchini-src/zucchini-demo) project featuring an easy to explore Zucchini setup around Apple's CoreDataBooks sample.
42
+ Make sure you check out the [zucchini-demo](https://github.com/zucchini-src/zucchini-demo) project featuring an easy to explore Zucchini setup around Apple's CoreDataBooks sample.
43
43
 
44
44
  ## Running on the device
45
45
 
@@ -50,53 +50,76 @@ The [udidetect](https://github.com/vaskas/udidetect) utility comes in handy if y
50
50
  ```
51
51
  ZUCCHINI_DEVICE="My Device" zucchini run /path/to/my_feature
52
52
  ```
53
+ You can set one of the devices to be used by default in `config.yml` so that you can avoid setting `ZUCCHINI_DEVICE` each time:
54
+
55
+ ```
56
+ devices:
57
+ My Device:
58
+ default: true
59
+ ...
60
+ ```
53
61
 
54
62
  ## Running on the iOS Simulator
55
63
 
56
64
  We encourage you to run your Zucchini features on real hardware. However you can also run them on the iOS Simulator.
57
65
 
58
- First off, modify your `features/support/config.yml` to include the path to your compiled app, e.g.
66
+ First off, modify your `features/support/config.yml` to include the path to your compiled app (relative or absolute), e.g.
59
67
 
60
68
  ```
61
69
  app: ./Build/Products/Debug-iphonesimulator/CoreDataBooks.app
62
70
  ```
63
71
 
64
- Secondly, add an `iOS Simulator` entry to the devices section (no UDID needed) and make sure you provide the actual value for 'screen' based on your iOS Simulator settings:
72
+ Secondly, add a simulator device entry (no UDID needed) and make sure you provide the actual value for `screen` based on your iOS Simulator settings:
65
73
 
66
74
  ```
67
75
  devices:
68
- iOS Simulator:
69
- screen: low_ios5
76
+ My Simulator:
77
+ screen: retina_ios7
78
+ simulator: iPhone (Retina 4-inch)
79
+ ...
70
80
  ```
71
81
 
72
- Alternatively, you can specify the app path in the device section:
82
+ You can also override the app path per device:
73
83
 
74
84
  ```
75
85
  devices:
76
- iOS Simulator:
77
- screen: low_ios5
78
- app: ./Build/Products/Debug-iphonesimulator/CoreDataBooks.app
79
86
  iPad2:
80
- screen: ipad_ios5
87
+ screen: ipad_ios6
81
88
  app: ./Build/Products/Debug-iphoneos/CoreDataBooks.app
82
89
  ```
83
90
 
84
- If you do not want to hard-code the app path in your config files, you can use the environment variable `ZUCCHINI_APP`:
91
+ Note that `config.yml` is compiled through ERB so that you can use environment variables, e.g.
85
92
 
86
- ```
87
- ZUCCHINI_APP="/path/to/app" zucchini...
93
+ ```erb
94
+ app: <%= ENV['ZUCCHINI_APP'] %>
88
95
  ```
89
96
 
97
+
90
98
  Run Zucchini and watch the simulator go!
91
99
 
92
100
  ```
93
- ZUCCHINI_DEVICE="iOS Simulator" zucchini run /path/to/my_feature
101
+ ZUCCHINI_DEVICE="My Simulator" zucchini run /path/to/my_feature
94
102
  ```
95
103
 
96
104
  ## See also
97
105
 
106
+ ### Built-in help
107
+
98
108
  ```
99
109
  zucchini --help
100
110
  zucchini run --help
101
111
  zucchini generate --help
102
112
  ```
113
+
114
+ ### Further reading
115
+
116
+ * [Zucchini features on the inside](https://github.com/zucchini-src/zucchini/wiki/Features-on-the-inside)
117
+ * [Continuous Integration with Zucchini](https://github.com/zucchini-src/zucchini/wiki/CI)
118
+ * [Automated iOS Testing with Zucchini](http://www.jacopretorius.net/2013/04/automated-ios-testing-with-zucchini.html) - a tutorial by [@Jaco-Pretorius](https://github.com/Jaco-Pretorius)
119
+ * [Zucchini Google Group](https://groups.google.com/forum/#!forum/zucchini-discuss)
120
+
121
+ ## Credits
122
+ * [Zucchini contributors](https://github.com/zucchini-src/zucchini/graphs/contributors) also known as the awesome [CHANGELOG](https://github.com/zucchini-src/zucchini/blob/master/CHANGELOG.md) guys
123
+ * [Rajesh Kumar](https://github.com/rajbeniwal) for alpha and beta testing, ideas and the initial feedback
124
+ * [Kevin O'Neill](https://github.com/kevinoneill) for the original idea and inspiration
125
+ * [PlayUp](http://www.playup.com/) where the project was born and first released.
@@ -7,6 +7,7 @@ module Zucchini
7
7
  require 'zucchini/compiler'
8
8
  require 'zucchini/device'
9
9
  require 'zucchini/feature'
10
+ require 'zucchini/log'
10
11
  require 'zucchini/detector'
11
12
  require 'zucchini/runner'
12
13
  require 'zucchini/generator'
@@ -1,6 +1,6 @@
1
1
  class Zucchini::Approver < Zucchini::Detector
2
2
  parameter "PATH", "a path to feature or a directory"
3
-
3
+
4
4
  option %W(-p --pending), :flag, "update pending screenshots instead"
5
5
 
6
6
  def run_command
@@ -11,4 +11,4 @@ class Zucchini::Approver < Zucchini::Detector
11
11
  end
12
12
  features.inject(true){ |result, feature| result &= feature.succeeded }
13
13
  end
14
- end
14
+ end
@@ -1,3 +1,5 @@
1
+ require 'fileutils'
2
+
1
3
  module Zucchini
2
4
  module Compiler
3
5
 
@@ -53,7 +55,7 @@ module Zucchini
53
55
  f.puts(File.read(js_path))
54
56
  end
55
57
 
56
- File.rename(tmp_file, js_path)
58
+ FileUtils.mv(tmp_file, js_path)
57
59
  end
58
60
  end
59
61
  end
@@ -5,24 +5,30 @@ class Zucchini::Feature
5
5
  attr_accessor :path
6
6
  attr_accessor :device
7
7
  attr_accessor :stats
8
+ attr_accessor :js_exception
8
9
 
9
10
  attr_reader :succeeded
10
11
  attr_reader :name
11
12
 
12
13
  def initialize(path)
13
- @path = path
14
- @device = nil
15
- @succeeded = false
16
- @name = File.basename(path)
14
+ @path = path
15
+ @name = File.basename(path)
16
+ @device = nil
17
+ @succeeded = false
18
+ @js_exception = false
17
19
  end
18
20
 
19
21
  def run_data_path
20
22
  "#{@path}/run_data"
21
23
  end
22
24
 
25
+ def run_path
26
+ "#{run_data_path}/Run\ 1"
27
+ end
28
+
23
29
  def unmatched_pending_screenshots
24
- Dir.glob("#{@path}/pending/#{@device[:screen]}/[^0-9]*.png").map do |file|
25
- screenshot = Zucchini::Screenshot.new(file, nil, true)
30
+ Dir.glob("#{@path}/pending/#{@device[:screen]}/[^0-9]*.png").sort.map do |file|
31
+ screenshot = Zucchini::Screenshot.new(file, nil, nil, true)
26
32
  screenshot.test_path = File.expand_path(file)
27
33
  screenshot.diff = [:pending, "unmatched"]
28
34
  screenshot
@@ -30,8 +36,10 @@ class Zucchini::Feature
30
36
  end
31
37
 
32
38
  def screenshots(process = true)
33
- @screenshots ||= Dir.glob("#{run_data_path}/Run\ 1/*.png").map do |file|
34
- screenshot = Zucchini::Screenshot.new(file, @device)
39
+ log = Zucchini::Log.new(run_path) if process && @screenshot_log_exists
40
+
41
+ @screenshots ||= Dir.glob("#{run_path}/*.png").sort.map do |file|
42
+ screenshot = Zucchini::Screenshot.new(file, @device, log)
35
43
  if process
36
44
  screenshot.mask
37
45
  screenshot.compare
@@ -58,16 +66,17 @@ class Zucchini::Feature
58
66
  -e UIARESULTSPATH "#{run_data_path}" 2>&1`
59
67
  puts out
60
68
  # Hack. Instruments don't issue error return codes when JS exceptions occur
61
- raise "Instruments run error" if (out.match /JavaScript error/) || (out.match /Instruments\ .{0,5}\ Error\ :/ )
69
+ @js_exception = true if (out.match /JavaScript error/) || (out.match /Instruments\ .{0,5}\ Error\ :/ )
62
70
  ensure
63
71
  `rm -rf instrumentscli*.trace`
72
+ @screenshot_log_exists = Zucchini::Log.parse_automation_log(run_path)
64
73
  end
65
74
  end
66
75
  end
67
76
 
68
77
  def compare
69
78
  `rm -rf #{run_data_path}/Diff/*`
70
- @succeeded = (stats[:failed].length == 0)
79
+ @succeeded = !@js_exception && (stats[:failed].length == 0)
71
80
  end
72
81
 
73
82
  def with_setup
@@ -0,0 +1,59 @@
1
+ require 'plist'
2
+
3
+ class Zucchini::Log
4
+ YAML_FILE = 'screenshots.yml'
5
+
6
+ attr_reader :screenshot_log_path
7
+
8
+ def initialize(path)
9
+ @screenshot_log_path = File.join(path, YAML_FILE)
10
+ raise "Screenshot log not found at #{@screenshot_log_path}" unless File.exists?(@screenshot_log_path)
11
+ @screenshots = File.open(@screenshot_log_path, 'r') { |f| YAML.load(f) }
12
+ end
13
+
14
+ def exists?
15
+ @screenshots != nil
16
+ end
17
+
18
+ def screenshot_metadata(sequence_number)
19
+ raise "Invalid screenshot sequence number #{sequence_number}" if sequence_number > @screenshots.size
20
+ @screenshots[sequence_number - 1]
21
+ end
22
+
23
+ def mark_screenshot_as_rotated(sequence_number)
24
+ screenshot_metadata(sequence_number)[:rotated] = true
25
+ end
26
+
27
+ def save
28
+ File.open(@screenshot_log_path, 'w') {|f| f.write(@screenshots.to_yaml) }
29
+ end
30
+
31
+ def self.parse_automation_log(path, screenshot_log_path = nil)
32
+ automation_log_path = File.join(path, 'Automation Results.plist')
33
+
34
+ if File.exists?(automation_log_path)
35
+ log = Plist::parse_xml(automation_log_path)
36
+ raise "Automation log at #{log_path} could not be parsed" unless log
37
+ entries = log["All Samples"]
38
+ screenshots = []
39
+
40
+ entries.each do |entry|
41
+ next unless entry['LogType'] == 'Default'
42
+ match = entry["Message"].match(/^Screenshot.*screen '(?<screen>[^']*)'.*orientation '(?<orientation>[^']*)'$/)
43
+
44
+ if match
45
+ metadata = {:screen => match[:screen], :orientation => match[:orientation] }
46
+ screenshots << metadata
47
+ end
48
+ end
49
+
50
+ screenshot_log_path ||= File.join(path, YAML_FILE)
51
+ File.open(screenshot_log_path, 'w') {|f| f.write(screenshots.to_yaml) }
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+ end
58
+
59
+
@@ -1,40 +1,23 @@
1
- require 'erb'
2
- require 'zucchini/report/view'
1
+ require 'zucchini/reporters/tap'
2
+ require 'zucchini/reporters/html'
3
3
 
4
4
  class Zucchini::Report
5
-
6
- def initialize(features, ci = false, html_path = '/tmp/zucchini_report.html')
7
- @features, @ci, @html_path = [features, ci, html_path]
8
- generate!
9
- end
10
-
11
- def text
12
- @features.map do |f|
13
- failed_list = f.stats[:failed].empty? ? "" : "\n\nFailed:\n" + f.stats[:failed].map { |s| " #{s.file_name}: #{s.diff[1]}" }.join
14
- summary = f.stats.map { |key, set| "#{set.length.to_s} #{key}" }.join(", ")
15
-
16
- "#{f.name}:\n#{summary}#{failed_list}"
17
- end.join("\n\n")
18
- end
19
-
20
- def html
21
- @html ||= begin
22
- template_path = File.expand_path("#{File.dirname(__FILE__)}/report/template.erb.html")
23
-
24
- view = Zucchini::ReportView.new(@features, @ci)
25
- compiled = (ERB.new(File.open(template_path).read)).result(view.get_binding)
26
-
27
- File.open(@html_path, 'w+') { |f| f.write(compiled) }
28
- compiled
29
- end
5
+ def initialize(features, ci = false, reports_dir)
6
+ FileUtils.mkdir_p(reports_dir)
7
+
8
+ @paths = {
9
+ :html => "#{reports_dir}/zucchini_report.html",
10
+ :tap => "#{reports_dir}/zucchini.t"
11
+ }
12
+ generate(features, ci, @paths)
30
13
  end
31
14
 
32
- def generate!
33
- log text
34
- html
15
+ def generate(features, ci, paths)
16
+ log Zucchini::Reporter::TAP.generate features, paths[:tap]
17
+ log Zucchini::Reporter::HTML.generate features, paths[:html], ci
35
18
  end
36
19
 
37
- def open; system "open #{@html_path}"; end
20
+ def open; system "open #{@paths[:html]}"; end
38
21
 
39
22
  def log(buf); puts buf; end
40
23
  end
@@ -0,0 +1,75 @@
1
+ require 'time'
2
+ require 'erb'
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ module Zucchini::Reporter
7
+ class HTML
8
+ def self.generate(features, report_path, ci)
9
+ Zucchini::Reporter::HTML.new(features, ci).write(report_path)
10
+ "HTML report generated to #{report_path}"
11
+ end
12
+
13
+ def initialize(features, ci)
14
+ @features = features
15
+ @device = features[0].device
16
+ @time = Time.now.strftime("%T, %e %B %Y")
17
+ @ci = ci ? 'ci' : ''
18
+ end
19
+
20
+ def write(report_path)
21
+ template_path = "#{File.dirname(__FILE__)}/html/template.erb.html"
22
+ gem_assets_dir = "#{File.dirname(__FILE__)}/html"
23
+
24
+ files_path = report_path.chomp(File.extname report_path) + '_files'
25
+
26
+ @assets_path = copy_assets(gem_assets_dir, "#{files_path}/assets", report_path)
27
+ @features = copy_images(@features, "#{files_path}/images", report_path)
28
+
29
+ File.open(report_path, 'w+') do |f|
30
+ f.write ERB.new(File.read(template_path)).result(binding)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def recreate_dir(path)
37
+ FileUtils.rm_rf path
38
+ FileUtils.mkdir_p path
39
+ end
40
+
41
+ def relative_path(dest_path, base_path)
42
+ Pathname.new(dest_path).relative_path_from(Pathname.new(base_path).dirname)
43
+ end
44
+
45
+ def copy_assets(src_dir, dest_dir, report_path)
46
+ recreate_dir(dest_dir)
47
+ %W(js css).each { |t| FileUtils.cp_r("#{src_dir}/#{t}", dest_dir) }
48
+
49
+ relative_path(dest_dir, report_path)
50
+ end
51
+
52
+ def copy_images(features, dest_dir, report_path)
53
+ recreate_dir(dest_dir)
54
+
55
+ features.each do |f|
56
+ f.screenshots.each do |s|
57
+ %W(actual expected difference).each do |type|
58
+ src_path = s.result_images[type.to_sym]
59
+ if src_path
60
+ name = File.basename(src_path)
61
+ type_dir = "#{dest_dir}/#{f.name}/#{type}"
62
+ dest_path = "#{type_dir}/#{name}"
63
+
64
+ FileUtils.mkdir_p(type_dir)
65
+ FileUtils.cp(src_path, dest_path)
66
+
67
+ s.result_images[type.to_sym] = relative_path(dest_path, report_path)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ end
75
+ end
@@ -35,7 +35,10 @@
35
35
  <dl class="<%= css_class %> <%= css_class == 'failed' ? 'expanded' :'' %> screen">
36
36
  <dt><%= s.file_name %></dt>
37
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>
38
+ <dd class="<%= s.result_images[type.to_sym] ? '' : 'hidden' %>">
39
+ <p><%= type.capitalize %></p>
40
+ <img src="<%= s.result_images[type.to_sym] %>"/>
41
+ </dd>
39
42
  <% end %>
40
43
  </dl>
41
44
  <% end %>
@@ -0,0 +1,30 @@
1
+ module Zucchini::Reporter
2
+ module TAP
3
+ extend self
4
+
5
+ def generate(features, report_path)
6
+ File.open(report_path, 'w+') do |io|
7
+ io.puts "1..#{features.length}"
8
+ features.each_with_index do |f, i|
9
+ io.puts (f.succeeded ? "ok" : "not ok") + " #{i + 1} - #{f.name}"
10
+ io.puts " 1..#{f.screenshots.length}"
11
+ f.screenshots.each_with_index do |s, j|
12
+ failed = s.diff[0] == :failed
13
+ pending = s.diff[0] == :pending
14
+
15
+ out = " "
16
+ out += failed ? "not ok" : "ok"
17
+ out += " #{j + 1} - #{s.file_name}"
18
+ out += failed ? " does not match (#{s.diff[1]})" : ''
19
+ out += pending ? " # pending" : ''
20
+
21
+ io.puts(out)
22
+ end
23
+ io.puts ' Bail out! Instruments run error' if f.js_exception
24
+ end
25
+ io.close
26
+ end
27
+ File.read(report_path) + "\nTAP report generated to #{report_path}"
28
+ end
29
+ end
30
+ end
@@ -4,9 +4,10 @@ class Zucchini::Runner < Zucchini::Detector
4
4
  option %W(-c --collect), :flag, "only collect the screenshots from the device"
5
5
  option %W(-p --compare), :flag, "perform screenshots comparison based on the last collection"
6
6
  option %W(-s --silent), :flag, "do not open the report"
7
-
8
7
  option "--ci", :flag, "produce a CI version of the report after comparison"
9
8
 
9
+ option %W(-r --reports-dir), "DIR", "specify the directory for generated reports" , :default => '/tmp'
10
+
10
11
  def run_command
11
12
  compare_threads = {}
12
13
 
@@ -22,7 +23,7 @@ class Zucchini::Runner < Zucchini::Detector
22
23
  compare_threads.each { |name, t| t.abort_on_exception = true; t.join }
23
24
 
24
25
  unless (collect? && !compare?)
25
- report = Zucchini::Report.new(features, ci?)
26
+ report = Zucchini::Report.new(features, ci?, reports_dir)
26
27
  report.open unless silent?
27
28
  end
28
29
 
@@ -1,54 +1,50 @@
1
1
  class Zucchini::Screenshot
2
- FILE_NAME_PATTERN = /^\d\d_((?<orientation>Unknown|Portrait|PortraitUpsideDown|LandscapeLeft|LandscapeRight|FaceUp|FaceDown)_)?((?<screen>.*)-screen_)?.*$/
2
+ FILE_NAME_PATTERN = /^(?<sequence_number>\d\d)_(?<screenshot_name>[^\.]*)\.png$/
3
3
 
4
4
  attr_reader :file_path, :original_file_path, :file_name
5
- attr_accessor :diff, :masks_paths, :masked_paths, :test_path, :diff_path
5
+ attr_accessor :diff, :mask_paths, :masked_paths, :test_path, :diff_path
6
6
 
7
- def initialize(file_path, device, unmatched_pending = false)
8
- @original_file_path = file_path
9
- @file_path = file_path.dup
10
-
7
+ def initialize(file_path, device, log, unmatched_pending = false)
8
+ @file_path = file_path
9
+ @log = log
11
10
  @device = device
12
11
 
13
- match = FILE_NAME_PATTERN.match(File.basename(@file_path))
12
+ @file_name = File.basename(@file_path)
13
+ match = FILE_NAME_PATTERN.match(@file_name)
14
+ raise "Illegal screenshot name #{file_path}" unless match
14
15
 
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
20
- end
16
+ @screenshot_name = match[:screenshot_name]
17
+ @sequence_number = match[:sequence_number].to_i
21
18
 
22
19
  @file_name = File.basename(@file_path)
23
20
 
24
21
  unless unmatched_pending
25
- file_base_path = File.dirname(@file_path)
22
+ run_data_path = File.dirname(@file_path)
23
+ support_path = File.join(run_data_path, '../../../support')
24
+
25
+ if @log
26
+ metadata = @log.screenshot_metadata(@sequence_number)
27
+ @orientation = metadata[:orientation]
28
+ @screen = metadata[:screen]
29
+ @rotated = metadata[:rotated]
30
+ end
26
31
 
27
- support_masks_path = "#{file_base_path}/../../../support/masks"
32
+ if @orientation && !@rotated
33
+ rotate
34
+ @log.mark_screenshot_as_rotated(@sequence_number)
35
+ @log.save
36
+ end
28
37
 
29
- @masks_paths = {
30
- :global => "#{support_masks_path}/#{@device[:screen]}.png",
31
- :screen => "#{support_masks_path}/#{@screen.to_s.underscore}.png",
32
- :specific => "#{file_base_path}/../../masks/#{@device[:screen]}/#{@file_name}"
38
+ @mask_paths = {
39
+ :global => mask_path(File.join(support_path, 'masks', @device[:screen])),
40
+ :specific => mask_path(File.join(run_data_path, '../../masks', @device[:screen], @file_name.sub('.png', ''))),
41
+ :screen => mask_path(File.join(support_path, 'screens/masks', @device[:screen], @screen.to_s.underscore))
33
42
  }
34
43
 
35
- masked_path = "#{file_base_path}/../Masked/actual/#{@file_name}"
44
+ masked_path = File.join(run_data_path, "../Masked/actual/#{@file_name}")
36
45
  @masked_paths = { :global => masked_path, :screen => masked_path, :specific => masked_path }
37
46
 
38
- @diff_path = "#{file_base_path}/../Diff/#{@file_name}"
39
- end
40
-
41
- preprocess
42
- end
43
-
44
- def preprocess
45
- return if @original_file_path == @file_path
46
-
47
- if @orientation
48
- rotate
49
- else
50
- FileUtils.rm @file_path if File.exists?(@file_path)
51
- FileUtils.mv @original_file_path, @file_path
47
+ @diff_path = "#{run_data_path}/../Diff/#{@file_name}"
52
48
  end
53
49
  end
54
50
 
@@ -56,11 +52,11 @@ class Zucchini::Screenshot
56
52
  create_masked_paths_dirs
57
53
  masked_path = apply_mask(@file_path, :global)
58
54
 
59
- if mask?(:screen)
55
+ if mask_present?(:screen)
60
56
  masked_path = apply_mask(masked_path, :screen)
61
57
  end
62
58
 
63
- if mask?(:specific)
59
+ if mask_present?(:specific)
64
60
  apply_mask(masked_path, :specific)
65
61
  end
66
62
  end
@@ -77,7 +73,7 @@ class Zucchini::Screenshot
77
73
  @diff = (out == '0') ? [:passed, nil] : [:failed, out]
78
74
  @diff = [:pending, @diff[1]] if @pending
79
75
  else
80
- @diff = [:failed, "no reference or pending screenshot for #{@device[:screen]}\n"]
76
+ @diff = [:failed, "no reference or pending screenshot for #{@device[:screen]}"]
81
77
  end
82
78
  end
83
79
 
@@ -90,18 +86,18 @@ class Zucchini::Screenshot
90
86
  end
91
87
 
92
88
  def mask_reference
93
- file_base_path = File.dirname(@file_path)
89
+ run_data_path = File.dirname(@file_path)
94
90
  %W(reference pending).each do |reference_type|
95
- reference_file_path = "#{file_base_path}/../../#{reference_type}/#{@device[:screen]}/#{@file_name}"
96
- output_path = "#{file_base_path}/../Masked/#{reference_type}/#{@file_name}"
91
+ reference_file_path = "#{run_data_path}/../../#{reference_type}/#{@device[:screen]}/#{@file_name}"
92
+ output_path = "#{run_data_path}/../Masked/#{reference_type}/#{@file_name}"
97
93
 
98
94
  if File.exists?(reference_file_path)
99
95
  @test_path = output_path
100
96
  @pending = (reference_type == "pending")
101
97
  FileUtils.mkdir_p(File.dirname(output_path))
102
98
 
103
- reference = Zucchini::Screenshot.new(reference_file_path, @device)
104
- reference.masks_paths = @masks_paths
99
+ reference = Zucchini::Screenshot.new(reference_file_path, @device, @log)
100
+ reference.mask_paths = @mask_paths
105
101
  reference.masked_paths = { :global => output_path, :screen => output_path, :specific => output_path }
106
102
  reference.mask
107
103
  end
@@ -109,8 +105,22 @@ class Zucchini::Screenshot
109
105
  end
110
106
 
111
107
  private
112
- def mask?(mask)
113
- @masks_paths[mask] && File.exists?(@masks_paths[mask])
108
+ def mask_path(path)
109
+ suffix = case @orientation
110
+ when 'LandscapeRight', 'LandscapeLeft' then '_landscape'
111
+ when 'Portrait', 'PortraitUpsideDown' then '_portrait'
112
+ else
113
+ ''
114
+ end
115
+
116
+ file_path = path + suffix + '.png'
117
+ file_path = path + '.png' unless File.exists?(file_path)
118
+
119
+ File.expand_path(file_path)
120
+ end
121
+
122
+ def mask_present?(mask)
123
+ @mask_paths[mask] && File.exists?(@mask_paths[mask])
114
124
  end
115
125
 
116
126
  def create_masked_paths_dirs
@@ -118,8 +128,8 @@ class Zucchini::Screenshot
118
128
  end
119
129
 
120
130
  def apply_mask(src_path, mask)
121
- mask_path = @masks_paths[mask]
122
- dest_path = @masked_paths[mask]
131
+ mask_path = @mask_paths[mask]
132
+ dest_path = @masked_paths[mask]
123
133
  `convert -page +0+0 \"#{src_path}\" -page +0+0 \"#{mask_path}\" -flatten \"#{dest_path}\"`
124
134
  return dest_path
125
135
  end
@@ -132,8 +142,9 @@ class Zucchini::Screenshot
132
142
  else
133
143
  0
134
144
  end
135
- `convert \"#{@original_file_path}\" -rotate \"#{degrees}\" \"#{@file_path}\"`
136
- FileUtils.rm @original_file_path
145
+
146
+ `mogrify -rotate \"#{degrees}\" \"#{@file_path}\"` if degrees > 0
147
+ @rotated = true
137
148
  end
138
149
  end
139
150
 
@@ -9,8 +9,8 @@ class Screen
9
9
  when 4 then 'LandscapeRight'
10
10
  when 5 then 'FaceUp'
11
11
  when 6 then 'FaceDown'
12
- $.log "Screenshot of screen '#{@name}' taken"
13
- target.captureScreenWithName("#{orientation}_#{@name}-screen_#{screenshotName}")
12
+ $.log "Screenshot of screen '#{@name}' taken with orientation '#{orientation}'"
13
+ target.captureScreenWithName(screenshotName)
14
14
 
15
15
  element: (name) ->
16
16
  finder = @elements[name] || -> $('#' + name)
@@ -1,3 +1,3 @@
1
1
  module Zucchini
2
- VERSION = "0.7.1"
2
+ VERSION = "0.7.2"
3
3
  end
@@ -1,6 +1,5 @@
1
- # Start on the "Welcome" screen:
2
- # Take a screenshot named "welcome_first_boot"
3
- # Type "Zucchini" in the username field
4
- # Tap "Go"
5
-
6
-
1
+ Start on the "Welcome" screen:
2
+ Take a screenshot named "welcome_first_boot"
3
+ Type "Zucchini" in the username field
4
+ Tap "Go"
5
+
@@ -1,12 +1,13 @@
1
1
  app: MyApp.app
2
2
 
3
3
  devices:
4
- My iDevice:
5
- UDID : lolffb28d74a6fraj2156090784avasc50725dd0
6
- screen: retina_ios5
4
+ iPhone Simulator:
5
+ default: true
6
+ simulator: iPhone (Retina 3.5-inch)
7
+ screen: retina_ios6
8
+ app: /Users/yourname/Library/Developer/Xcode/DerivedData/MyApp-fofsuperjgolongsfzappyoeidss/Build/Products/Debug-iphonesimulator/MyApp.app
7
9
 
10
+ iPhone 4S:
11
+ UDID: lolffb28d74a6fraj2156090784avasc50725dd0
12
+ screen: retina_ios6
8
13
 
9
- servers:
10
- backend:
11
- host: 192.168.1.2
12
- port: 8080
@@ -1,3 +1,3 @@
1
1
  class Helpers
2
2
  @example: ->
3
- puts "Helpers.example method is available in your screen classes"
3
+ $.log "Helpers.example method is available in your screen classes"
@@ -1,13 +1,13 @@
1
1
  class WelcomeScreen extends Screen
2
- anchor: -> view.elements()["Welcome to MyApp"]
3
-
2
+ anchor: -> $("navigationBar[name=Welcome]")
3
+
4
4
  constructor: ->
5
5
  super 'welcome'
6
-
6
+
7
7
  extend @elements,
8
8
  'Go' : -> view.buttons()["Go"]
9
-
9
+
10
10
  extend @actions,
11
11
  'Type "([^"]*)" in the username field$': (text) ->
12
12
  field = view.elements()['Username']
13
- field.setValue text
13
+ field.setValue text
@@ -6,14 +6,15 @@ require 'zucchini/version'
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "zucchini-ios"
8
8
  s.version = Zucchini::VERSION
9
- s.authors = ["Vasily Mikhaylichenko", "Rajesh Kumar", "Kevin O'Neill"]
9
+ s.authors = ["Vasily Mikhaylichenko"]
10
10
  s.licenses = %w{ BSD MIT }
11
- s.email = ["vaskas@zucchiniframework.org"]
11
+ s.email = ["vaskas@lxmx.com.au"]
12
12
  s.homepage = "http://www.zucchiniframework.org"
13
13
  s.summary = %q{A visual iOS testing framework}
14
14
  s.description = %q{Zucchini follows simple walkthrough scenarios for your iOS app, takes screenshots and compares them to the reference ones.}
15
15
 
16
16
  s.add_runtime_dependency 'clamp'
17
+ s.add_runtime_dependency 'plist'
17
18
  s.add_development_dependency 'rspec'
18
19
  s.add_development_dependency 'simplecov'
19
20
  s.add_development_dependency 'coveralls'
metadata CHANGED
@@ -1,17 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zucchini-ios
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Vasily Mikhaylichenko
9
- - Rajesh Kumar
10
- - Kevin O'Neill
11
9
  autorequire:
12
10
  bindir: bin
13
11
  cert_chain: []
14
- date: 2013-08-11 00:00:00.000000000 Z
12
+ date: 2013-08-18 00:00:00.000000000 Z
15
13
  dependencies:
16
14
  - !ruby/object:Gem::Dependency
17
15
  name: clamp
@@ -29,6 +27,22 @@ dependencies:
29
27
  - - ! '>='
30
28
  - !ruby/object:Gem::Version
31
29
  version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: plist
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
32
46
  - !ruby/object:Gem::Dependency
33
47
  name: rspec
34
48
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +94,7 @@ dependencies:
80
94
  description: Zucchini follows simple walkthrough scenarios for your iOS app, takes
81
95
  screenshots and compares them to the reference ones.
82
96
  email:
83
- - vaskas@zucchiniframework.org
97
+ - vaskas@lxmx.com.au
84
98
  executables:
85
99
  - zucchini
86
100
  extensions: []
@@ -102,14 +116,16 @@ files:
102
116
  - lib/zucchini/device.rb
103
117
  - lib/zucchini/feature.rb
104
118
  - lib/zucchini/generator.rb
119
+ - lib/zucchini/log.rb
105
120
  - lib/zucchini/report.rb
106
- - lib/zucchini/report/css/zucchini.report.css
107
- - lib/zucchini/report/js/jquery.effects.core.js
108
- - lib/zucchini/report/js/jquery.js
109
- - lib/zucchini/report/js/jquery.ui.core.js
110
- - lib/zucchini/report/js/zucchini.report.js
111
- - lib/zucchini/report/template.erb.html
112
- - lib/zucchini/report/view.rb
121
+ - lib/zucchini/reporters/html.rb
122
+ - lib/zucchini/reporters/html/css/zucchini.report.css
123
+ - lib/zucchini/reporters/html/js/jquery.effects.core.js
124
+ - lib/zucchini/reporters/html/js/jquery.js
125
+ - lib/zucchini/reporters/html/js/jquery.ui.core.js
126
+ - lib/zucchini/reporters/html/js/zucchini.report.js
127
+ - lib/zucchini/reporters/html/template.erb.html
128
+ - lib/zucchini/reporters/tap.rb
113
129
  - lib/zucchini/runner.rb
114
130
  - lib/zucchini/screenshot.rb
115
131
  - lib/zucchini/uia/lib/compat.coffee
@@ -120,17 +136,16 @@ files:
120
136
  - lib/zucchini/uia/zucchini.coffee
121
137
  - lib/zucchini/version.rb
122
138
  - templates/feature/feature.zucchini
123
- - templates/feature/masks/retina_ios5/.gitkeep
124
- - templates/feature/pending/retina_ios5/.gitkeep
125
- - templates/feature/reference/retina_ios5/.gitkeep
139
+ - templates/feature/masks/retina_ios6/.gitkeep
140
+ - templates/feature/pending/retina_ios6/.gitkeep
141
+ - templates/feature/reference/retina_ios6/.gitkeep
126
142
  - templates/feature/run_data/.gitignore
127
143
  - templates/feature/setup.rb
128
144
  - templates/project/features/support/config.yml
129
145
  - templates/project/features/support/lib/helpers.coffee
130
- - templates/project/features/support/masks/ipad_ios5.png
146
+ - templates/project/features/support/masks/ipad_ios6.png
131
147
  - templates/project/features/support/masks/ipad_retina_ios6.png
132
- - templates/project/features/support/masks/low_ios4.png
133
- - templates/project/features/support/masks/retina_ios5.png
148
+ - templates/project/features/support/masks/retina_ios6.png
134
149
  - templates/project/features/support/screens/welcome.coffee
135
150
  - zucchini-ios.gemspec
136
151
  homepage: http://www.zucchiniframework.org
@@ -155,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
170
  version: '0'
156
171
  segments:
157
172
  - 0
158
- hash: 1435969441614600881
173
+ hash: 3231635061301993483
159
174
  requirements: []
160
175
  rubyforge_project:
161
176
  rubygems_version: 1.8.23
@@ -1,16 +0,0 @@
1
- require 'time'
2
-
3
- class Zucchini::ReportView
4
-
5
- def initialize(features, ci)
6
- @features = features
7
- @device = features[0].device
8
- @time = Time.now.strftime("%T, %e %B %Y")
9
- @assets_path = File.expand_path(File.dirname(__FILE__))
10
- @ci = ci ? 'ci' : ''
11
- end
12
-
13
- def get_binding
14
- binding
15
- end
16
- end