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.
- data/CHANGELOG.md +13 -0
- data/README.md +40 -17
- data/lib/zucchini.rb +1 -0
- data/lib/zucchini/approver.rb +2 -2
- data/lib/zucchini/compiler.rb +3 -1
- data/lib/zucchini/feature.rb +19 -10
- data/lib/zucchini/log.rb +59 -0
- data/lib/zucchini/report.rb +14 -31
- data/lib/zucchini/reporters/html.rb +75 -0
- data/lib/zucchini/{report → reporters/html}/css/zucchini.report.css +0 -0
- data/lib/zucchini/{report → reporters/html}/js/jquery.effects.core.js +0 -0
- data/lib/zucchini/{report → reporters/html}/js/jquery.js +0 -0
- data/lib/zucchini/{report → reporters/html}/js/jquery.ui.core.js +0 -0
- data/lib/zucchini/{report → reporters/html}/js/zucchini.report.js +0 -0
- data/lib/zucchini/{report → reporters/html}/template.erb.html +4 -1
- data/lib/zucchini/reporters/tap.rb +30 -0
- data/lib/zucchini/runner.rb +3 -2
- data/lib/zucchini/screenshot.rb +59 -48
- data/lib/zucchini/uia/screen.coffee +2 -2
- data/lib/zucchini/version.rb +1 -1
- data/templates/feature/feature.zucchini +5 -6
- data/templates/feature/masks/{retina_ios5 → retina_ios6}/.gitkeep +0 -0
- data/templates/feature/pending/{retina_ios5 → retina_ios6}/.gitkeep +0 -0
- data/templates/feature/reference/{retina_ios5 → retina_ios6}/.gitkeep +0 -0
- data/templates/project/features/support/config.yml +8 -7
- data/templates/project/features/support/lib/helpers.coffee +1 -1
- data/templates/project/features/support/masks/{ipad_ios5.png → ipad_ios6.png} +0 -0
- data/templates/project/features/support/masks/{retina_ios5.png → retina_ios6.png} +0 -0
- data/templates/project/features/support/screens/welcome.coffee +5 -5
- data/zucchini-ios.gemspec +3 -2
- metadata +34 -19
- data/lib/zucchini/report/view.rb +0 -16
- data/templates/project/features/support/masks/low_ios4.png +0 -0
data/CHANGELOG.md
CHANGED
@@ -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
|
-
|
28
|
+
To create a project scaffold:
|
29
29
|
|
30
30
|
```
|
31
31
|
zucchini generate --project /path/to/my_project
|
32
32
|
```
|
33
33
|
|
34
|
-
|
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
|
40
|
+
Start developing by editing `features/my_feature/feature.zucchini` and `features/support/screens/welcome.coffee`.
|
41
41
|
|
42
|
-
|
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
|
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
|
-
|
69
|
-
screen:
|
76
|
+
My Simulator:
|
77
|
+
screen: retina_ios7
|
78
|
+
simulator: iPhone (Retina 4-inch)
|
79
|
+
...
|
70
80
|
```
|
71
81
|
|
72
|
-
|
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:
|
87
|
+
screen: ipad_ios6
|
81
88
|
app: ./Build/Products/Debug-iphoneos/CoreDataBooks.app
|
82
89
|
```
|
83
90
|
|
84
|
-
|
91
|
+
Note that `config.yml` is compiled through ERB so that you can use environment variables, e.g.
|
85
92
|
|
86
|
-
```
|
87
|
-
|
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="
|
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.
|
data/lib/zucchini.rb
CHANGED
data/lib/zucchini/approver.rb
CHANGED
@@ -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
|
data/lib/zucchini/compiler.rb
CHANGED
data/lib/zucchini/feature.rb
CHANGED
@@ -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
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
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
|
-
|
34
|
-
|
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
|
-
|
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
|
data/lib/zucchini/log.rb
ADDED
@@ -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
|
+
|
data/lib/zucchini/report.rb
CHANGED
@@ -1,40 +1,23 @@
|
|
1
|
-
require '
|
2
|
-
require 'zucchini/
|
1
|
+
require 'zucchini/reporters/tap'
|
2
|
+
require 'zucchini/reporters/html'
|
3
3
|
|
4
4
|
class Zucchini::Report
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
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 #{@
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -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' %>"
|
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
|
data/lib/zucchini/runner.rb
CHANGED
@@ -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
|
|
data/lib/zucchini/screenshot.rb
CHANGED
@@ -1,54 +1,50 @@
|
|
1
1
|
class Zucchini::Screenshot
|
2
|
-
FILE_NAME_PATTERN =
|
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, :
|
5
|
+
attr_accessor :diff, :mask_paths, :masked_paths, :test_path, :diff_path
|
6
6
|
|
7
|
-
def initialize(file_path, device, unmatched_pending = false)
|
8
|
-
@
|
9
|
-
@
|
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
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
32
|
+
if @orientation && !@rotated
|
33
|
+
rotate
|
34
|
+
@log.mark_screenshot_as_rotated(@sequence_number)
|
35
|
+
@log.save
|
36
|
+
end
|
28
37
|
|
29
|
-
@
|
30
|
-
:global =>
|
31
|
-
:
|
32
|
-
:
|
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 = "
|
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 = "#{
|
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
|
55
|
+
if mask_present?(:screen)
|
60
56
|
masked_path = apply_mask(masked_path, :screen)
|
61
57
|
end
|
62
58
|
|
63
|
-
if
|
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]}
|
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
|
-
|
89
|
+
run_data_path = File.dirname(@file_path)
|
94
90
|
%W(reference pending).each do |reference_type|
|
95
|
-
reference_file_path = "#{
|
96
|
-
output_path = "#{
|
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.
|
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
|
113
|
-
|
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
|
122
|
-
dest_path
|
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
|
-
|
136
|
-
|
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(
|
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)
|
data/lib/zucchini/version.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
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
|
+
|
File without changes
|
File without changes
|
File without changes
|
@@ -1,12 +1,13 @@
|
|
1
1
|
app: MyApp.app
|
2
2
|
|
3
3
|
devices:
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
File without changes
|
File without changes
|
@@ -1,13 +1,13 @@
|
|
1
1
|
class WelcomeScreen extends Screen
|
2
|
-
anchor: ->
|
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
|
data/zucchini-ios.gemspec
CHANGED
@@ -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"
|
9
|
+
s.authors = ["Vasily Mikhaylichenko"]
|
10
10
|
s.licenses = %w{ BSD MIT }
|
11
|
-
s.email = ["vaskas@
|
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.
|
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-
|
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@
|
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/
|
107
|
-
- lib/zucchini/
|
108
|
-
- lib/zucchini/
|
109
|
-
- lib/zucchini/
|
110
|
-
- lib/zucchini/
|
111
|
-
- lib/zucchini/
|
112
|
-
- lib/zucchini/
|
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/
|
124
|
-
- templates/feature/pending/
|
125
|
-
- templates/feature/reference/
|
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/
|
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/
|
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:
|
173
|
+
hash: 3231635061301993483
|
159
174
|
requirements: []
|
160
175
|
rubyforge_project:
|
161
176
|
rubygems_version: 1.8.23
|
data/lib/zucchini/report/view.rb
DELETED
@@ -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
|
Binary file
|