zucchini-ios 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|