zucchini-ios 0.5.5 → 0.5.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +28 -0
- data/README.md +25 -10
- data/lib/config.rb +20 -14
- data/lib/feature.rb +1 -1
- data/lib/runner.rb +28 -23
- data/lib/screenshot.rb +23 -27
- data/lib/uia/base.coffee +9 -0
- data/lib/version.rb +1 -1
- data/spec/sample_setup/support/config.yml +1 -1
- data/templates/project/features/support/config.yml +1 -1
- metadata +23 -9
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
zucchini-ios (0.5.5)
|
5
|
+
clamp
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
clamp (0.5.0)
|
11
|
+
diff-lcs (1.1.3)
|
12
|
+
rspec (2.11.0)
|
13
|
+
rspec-core (~> 2.11.0)
|
14
|
+
rspec-expectations (~> 2.11.0)
|
15
|
+
rspec-mocks (~> 2.11.0)
|
16
|
+
rspec-core (2.11.1)
|
17
|
+
rspec-expectations (2.11.2)
|
18
|
+
diff-lcs (~> 1.1.3)
|
19
|
+
rspec-mocks (2.11.2)
|
20
|
+
watchr (0.7)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
ruby
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
rspec
|
27
|
+
watchr
|
28
|
+
zucchini-ios!
|
data/README.md
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
Pre-requisites
|
2
2
|
--------------
|
3
|
-
1.
|
4
|
-
2.
|
3
|
+
1. Mac OS X >= 10.6
|
4
|
+
2. XCode >= 4.2
|
5
|
+
3. Ruby >= 1.9.2
|
6
|
+
4. A few command line tools:
|
5
7
|
|
6
8
|
```
|
7
|
-
brew update && brew install imagemagick && brew install coffee-script
|
9
|
+
brew update && brew install imagemagick && brew install coffee-script
|
8
10
|
```
|
9
11
|
|
10
12
|
Start using Zucchini
|
@@ -22,7 +24,7 @@ Start by creating a project scaffold:
|
|
22
24
|
zucchini generate --project /path/to/my_project
|
23
25
|
```
|
24
26
|
|
25
|
-
Create a feature scaffold for your first feature:
|
27
|
+
Create a feature scaffold for your first feature:
|
26
28
|
|
27
29
|
```
|
28
30
|
zucchini generate --feature /path/to/my_project/features/my_feature
|
@@ -39,17 +41,17 @@ Add your device to features/support/config.yml.
|
|
39
41
|
The [udidetect](https://github.com/vaskas/udidetect) utility comes in handy if you plan to add devices from time to time: `udidetect -z`.
|
40
42
|
|
41
43
|
```
|
42
|
-
ZUCCHINI_DEVICE="My Device" zucchini run /path/to/my_feature
|
44
|
+
ZUCCHINI_DEVICE="My Device" zucchini run /path/to/my_feature
|
43
45
|
```
|
44
46
|
|
45
47
|
Running on the iOS Simulator
|
46
48
|
-------------------------------
|
47
49
|
We strongly encourage you to run your Zucchini features on real hardware. However, you can run them on the iOS Simulator if you must.
|
48
50
|
|
49
|
-
First off, modify your features/support/config.yml to include
|
51
|
+
First off, modify your features/support/config.yml to include the path to your compiled app, e.g.
|
50
52
|
|
51
53
|
```
|
52
|
-
app:
|
54
|
+
app: ./Build/Products/Debug-iphonesimulator/CoreDataBooks.app
|
53
55
|
```
|
54
56
|
|
55
57
|
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:
|
@@ -60,16 +62,29 @@ devices:
|
|
60
62
|
screen: low_ios5
|
61
63
|
```
|
62
64
|
|
65
|
+
Alternatively, you can specify the app path in the device section:
|
66
|
+
|
67
|
+
```
|
68
|
+
devices:
|
69
|
+
iOS Simulator:
|
70
|
+
screen: low_ios5
|
71
|
+
app: ./Build/Products/Debug-iphonesimulator/CoreDataBooks.app
|
72
|
+
iPad2:
|
73
|
+
screen: ipad_ios5
|
74
|
+
app: ./Build/Products/Debug-iphoneos/CoreDataBooks.app
|
75
|
+
```
|
76
|
+
|
77
|
+
|
63
78
|
Run it as usual:
|
64
79
|
|
65
80
|
```
|
66
|
-
ZUCCHINI_DEVICE="iOS Simulator" zucchini run /path/to/my_feature
|
81
|
+
ZUCCHINI_DEVICE="iOS Simulator" zucchini run /path/to/my_feature
|
67
82
|
```
|
68
83
|
|
69
84
|
See also
|
70
85
|
---------
|
71
86
|
```
|
72
|
-
zucchini --help
|
73
|
-
zucchini run --help
|
87
|
+
zucchini --help
|
88
|
+
zucchini run --help
|
74
89
|
zucchini generate --help
|
75
90
|
```
|
data/lib/config.rb
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
|
3
3
|
module Zucchini
|
4
|
-
class Config
|
5
|
-
|
4
|
+
class Config
|
5
|
+
|
6
6
|
def self.base_path
|
7
7
|
@@base_path
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
def self.base_path=(base_path)
|
11
11
|
@@base_path = base_path
|
12
12
|
@@config = YAML::load_file("#{base_path}/support/config.yml")
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def self.app
|
16
|
-
|
16
|
+
device_name = ENV['ZUCCHINI_DEVICE']
|
17
|
+
app_path = File.absolute_path(devices[device_name]['app'] || @@config['app'])
|
18
|
+
|
19
|
+
if device_name == 'iOS Simulator' && !File.exists?(app_path)
|
20
|
+
raise "Can't find application at path #{app_path}"
|
21
|
+
end
|
22
|
+
app_path
|
17
23
|
end
|
18
|
-
|
19
|
-
def self.resolution_name(dimension)
|
24
|
+
|
25
|
+
def self.resolution_name(dimension)
|
20
26
|
@@config['resolutions'][dimension.to_i]
|
21
27
|
end
|
22
|
-
|
28
|
+
|
23
29
|
def self.devices
|
24
30
|
@@config['devices']
|
25
31
|
end
|
26
|
-
|
32
|
+
|
27
33
|
def self.device(device_name)
|
28
34
|
raise "Device not listed in config.yml" unless (device = devices[device_name])
|
29
35
|
{
|
@@ -32,16 +38,16 @@ module Zucchini
|
|
32
38
|
:screen => device['screen']
|
33
39
|
}
|
34
40
|
end
|
35
|
-
|
41
|
+
|
36
42
|
def self.server(server_name)
|
37
43
|
@@config['servers'][server_name]
|
38
44
|
end
|
39
|
-
|
45
|
+
|
40
46
|
def self.url(server_name, href="")
|
41
47
|
server_config = server(server_name)
|
42
48
|
port = server_config['port'] ? ":#{server_config['port']}" : ""
|
43
|
-
|
49
|
+
|
44
50
|
"http://#{server_config['host']}#{port}#{href}"
|
45
51
|
end
|
46
|
-
end
|
47
|
-
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/feature.rb
CHANGED
@@ -64,7 +64,7 @@ class Zucchini::Feature
|
|
64
64
|
device_params = (@device[:name] == "iOS Simulator") ? "" : "-w #{@device[:udid]}"
|
65
65
|
|
66
66
|
begin
|
67
|
-
out = `instruments #{device_params} -t #{@template} #{Zucchini::Config.app} -e UIASCRIPT #{run_data_path}/feature.js -e UIARESULTSPATH #{run_data_path} 2>&1`
|
67
|
+
out = `instruments #{device_params} -t "#{@template}" "#{Zucchini::Config.app}" -e UIASCRIPT "#{run_data_path}/feature.js" -e UIARESULTSPATH "#{run_data_path}" 2>&1`
|
68
68
|
puts out
|
69
69
|
# Hack. Instruments don't issue error return codes when JS exceptions occur
|
70
70
|
raise "Instruments run error" if (out.match /JavaScript error/) || (out.match /Instruments\ .{0,5}\ Error\ :/ )
|
data/lib/runner.rb
CHANGED
@@ -1,56 +1,56 @@
|
|
1
1
|
class Zucchini::Runner < Clamp::Command
|
2
2
|
attr_reader :features
|
3
|
-
|
3
|
+
|
4
4
|
parameter "PATH", "a path to feature or a directory to run"
|
5
|
-
|
5
|
+
|
6
6
|
option %W(-c --collect), :flag, "only collect the screenshots from the device"
|
7
7
|
option %W(-p --compare), :flag, "perform screenshots comparison based on the last collection"
|
8
8
|
option "--ci", :flag, "produce a CI version of the report after comparison"
|
9
9
|
|
10
10
|
def execute
|
11
11
|
raise "Directory #{path} does not exist" unless File.exists?(path)
|
12
|
-
|
12
|
+
|
13
13
|
@path = File.expand_path(path)
|
14
14
|
Zucchini::Config.base_path = File.exists?("#{path}/feature.zucchini") ? File.dirname(path) : path
|
15
|
-
|
15
|
+
|
16
16
|
raise "ZUCCHINI_DEVICE environment variable not set" unless ENV['ZUCCHINI_DEVICE']
|
17
|
-
@device = Zucchini::Config.device(ENV['ZUCCHINI_DEVICE'])
|
18
|
-
|
17
|
+
@device = Zucchini::Config.device(ENV['ZUCCHINI_DEVICE'])
|
18
|
+
|
19
19
|
@template = detect_template
|
20
|
-
|
21
|
-
exit
|
20
|
+
|
21
|
+
exit run_features
|
22
22
|
end
|
23
|
-
|
24
|
-
def
|
23
|
+
|
24
|
+
def run_features
|
25
25
|
compare_threads = {}
|
26
|
-
|
26
|
+
|
27
27
|
features.each do |f|
|
28
28
|
f.device = @device
|
29
29
|
f.template = @template
|
30
|
-
|
30
|
+
|
31
31
|
if collect? then f.collect
|
32
32
|
elsif compare? then f.compare
|
33
33
|
else f.collect; compare_threads[f.name] = Thread.new { f.compare }
|
34
34
|
end
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
compare_threads.each { |name, t| t.abort_on_exception = true; t.join }
|
38
|
-
|
38
|
+
|
39
39
|
Zucchini::Report.present(features, ci?) unless (collect? && !compare?)
|
40
40
|
features.inject(true){ |result, feature| result &= feature.succeeded }
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
def features
|
44
44
|
@features ||= detect_features(@path)
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
def detect_features(path)
|
48
48
|
features = []
|
49
49
|
if File.exists?("#{path}/feature.zucchini")
|
50
50
|
features << Zucchini::Feature.new(path)
|
51
51
|
else
|
52
52
|
raise detection_error(path) if Dir["#{path}/*"].empty?
|
53
|
-
|
53
|
+
|
54
54
|
Dir.glob("#{path}/*").each do |dir|
|
55
55
|
unless dir.match /support/
|
56
56
|
if File.exists?("#{dir}/feature.zucchini")
|
@@ -63,15 +63,20 @@ class Zucchini::Runner < Clamp::Command
|
|
63
63
|
end
|
64
64
|
features
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
def detection_error(path)
|
68
68
|
"#{path} is not a feature directory"
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
def detect_template
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
locations = [
|
73
|
+
`xcode-select -print-path`.gsub(/\n/, '') + "/Platforms/iPhoneOS.platform/Developer/Library/Instruments",
|
74
|
+
"/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents" # Xcode 4.5
|
75
|
+
].map do |start_path|
|
76
|
+
"#{start_path}/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate"
|
77
|
+
end
|
78
|
+
|
79
|
+
locations.each { |path| return path if File.exists?(path) }
|
80
|
+
raise "Can't find Instruments template (tried #{locations.join(', ')})"
|
76
81
|
end
|
77
82
|
end
|
data/lib/screenshot.rb
CHANGED
@@ -1,58 +1,54 @@
|
|
1
1
|
class Zucchini::Screenshot
|
2
|
-
attr_reader :
|
3
|
-
|
4
|
-
|
5
|
-
attr_accessor :diff
|
6
|
-
attr_accessor :masks_paths
|
7
|
-
attr_accessor :masked_paths
|
8
|
-
attr_accessor :test_path
|
9
|
-
attr_accessor :diff_path
|
10
|
-
|
2
|
+
attr_reader :file_path, :file_name
|
3
|
+
attr_accessor :diff, :masks_paths, :masked_paths, :test_path, :diff_path, :compare_cmd
|
4
|
+
|
11
5
|
def initialize(file_path, device, unmatched_pending = false)
|
12
6
|
@file_path = file_path
|
13
7
|
@file_name = File.basename(@file_path)
|
14
8
|
@device = device
|
15
|
-
|
9
|
+
|
10
|
+
@compare_cmd = "compare -metric AE -fuzz 2% -dissimilarity-threshold 1 -subimage-search"
|
11
|
+
|
16
12
|
unless unmatched_pending
|
17
13
|
@file_base_path = File.dirname(@file_path)
|
18
|
-
|
14
|
+
|
19
15
|
@masks_paths = {
|
20
16
|
:global => "#{@file_base_path}/../../../support/masks/#{@device[:screen]}.png",
|
21
|
-
:specific => "#{@file_base_path}/../../masks/#{@device[:screen]}/#{@file_name}"
|
17
|
+
:specific => "#{@file_base_path}/../../masks/#{@device[:screen]}/#{@file_name}"
|
22
18
|
}
|
23
|
-
|
19
|
+
|
24
20
|
masked_path = "#{@file_base_path}/../Masked/actual/#{@file_name}"
|
25
21
|
@masked_paths = { :globally => masked_path, :specifically => masked_path }
|
26
|
-
|
22
|
+
|
27
23
|
@test_path = nil
|
28
24
|
@pending = false
|
29
25
|
@diff_path = "#{@file_base_path}/../Diff/#{@file_name}"
|
30
|
-
end
|
26
|
+
end
|
31
27
|
end
|
32
|
-
|
28
|
+
|
33
29
|
def mask
|
34
30
|
@masked_paths.each { |name, path| FileUtils.mkdir_p(File.dirname(path)) }
|
35
31
|
`convert -page +0+0 \"#{@file_path}\" -page +0+0 \"#{@masks_paths[:global]}\" -flatten \"#{@masked_paths[:globally]}\"`
|
36
|
-
|
32
|
+
|
37
33
|
if File.exists?(@masks_paths[:specific])
|
38
34
|
`convert -page +0+0 \"#{@masked_paths[:globally]}\" -page +0+0 \"#{@masks_paths[:specific]}\" -flatten \"#{@masked_paths[:specifically]}\"`
|
39
35
|
end
|
40
36
|
end
|
41
|
-
|
37
|
+
|
42
38
|
def compare
|
43
39
|
mask_reference
|
44
|
-
|
40
|
+
|
45
41
|
if @test_path
|
46
42
|
FileUtils.mkdir_p(File.dirname(@diff_path))
|
47
|
-
|
48
|
-
out =
|
43
|
+
|
44
|
+
out = `#{@compare_cmd} \"#{@masked_paths[:specifically]}\" \"#{@test_path}\" \"#{@diff_path}\" 2>&1`
|
49
45
|
@diff = (out == "0\n") ? [:passed, nil] : [:failed, out]
|
50
46
|
@diff = [:pending, @diff[1]] if @pending
|
51
47
|
else
|
52
48
|
@diff = [:failed, "no reference or pending screenshot for #{@device[:screen]}\n"]
|
53
49
|
end
|
54
50
|
end
|
55
|
-
|
51
|
+
|
56
52
|
def result_images
|
57
53
|
@result_images ||= {
|
58
54
|
:actual => @masked_paths && File.exists?(@masked_paths[:specifically]) ? @masked_paths[:specifically] : nil,
|
@@ -60,17 +56,17 @@ class Zucchini::Screenshot
|
|
60
56
|
:difference => @diff_path && File.exists?(@diff_path) ? @diff_path : nil
|
61
57
|
}
|
62
58
|
end
|
63
|
-
|
59
|
+
|
64
60
|
def mask_reference
|
65
61
|
%W(reference pending).each do |reference_type|
|
66
62
|
reference_file_path = "#{@file_base_path}/../../#{reference_type}/#{@device[:screen]}/#{@file_name}"
|
67
63
|
output_path = "#{@file_base_path}/../Masked/#{reference_type}/#{@file_name}"
|
68
|
-
|
64
|
+
|
69
65
|
if File.exists?(reference_file_path)
|
70
66
|
@test_path = output_path
|
71
|
-
@pending = (reference_type == "pending")
|
67
|
+
@pending = (reference_type == "pending")
|
72
68
|
FileUtils.mkdir_p(File.dirname(output_path))
|
73
|
-
|
69
|
+
|
74
70
|
reference = Zucchini::Screenshot.new(reference_file_path, @device)
|
75
71
|
reference.masks_paths = @masks_paths
|
76
72
|
reference.masked_paths = { :globally => output_path, :specifically => output_path }
|
@@ -78,5 +74,5 @@ class Zucchini::Screenshot
|
|
78
74
|
end
|
79
75
|
end
|
80
76
|
end
|
81
|
-
|
77
|
+
|
82
78
|
end
|
data/lib/uia/base.coffee
CHANGED
@@ -19,6 +19,15 @@ target = UIATarget.localTarget()
|
|
19
19
|
app = target.frontMostApp()
|
20
20
|
view = app.mainWindow()
|
21
21
|
|
22
|
+
UIAElement.prototype.$ = (name) ->
|
23
|
+
target.pushTimeout(0)
|
24
|
+
elem = null
|
25
|
+
for el in this.elements()
|
26
|
+
elem = if el.name() == name then el else el.$(name)
|
27
|
+
break if elem
|
28
|
+
target.popTimeout()
|
29
|
+
elem
|
30
|
+
|
22
31
|
target.waitForElement = (element) ->
|
23
32
|
return unless element
|
24
33
|
found = false
|
data/lib/version.rb
CHANGED
metadata
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zucchini-ios
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
5
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 5
|
8
|
+
- 6
|
9
|
+
version: 0.5.6
|
6
10
|
platform: ruby
|
7
11
|
authors:
|
8
12
|
- Vasily Mikhaylichenko
|
@@ -12,16 +16,18 @@ autorequire:
|
|
12
16
|
bindir: bin
|
13
17
|
cert_chain: []
|
14
18
|
|
15
|
-
date: 2012-
|
19
|
+
date: 2012-12-03 00:00:00 +11:00
|
20
|
+
default_executable:
|
16
21
|
dependencies:
|
17
22
|
- !ruby/object:Gem::Dependency
|
18
23
|
name: clamp
|
19
24
|
prerelease: false
|
20
25
|
requirement: &id001 !ruby/object:Gem::Requirement
|
21
|
-
none: false
|
22
26
|
requirements:
|
23
27
|
- - ">="
|
24
28
|
- !ruby/object:Gem::Version
|
29
|
+
segments:
|
30
|
+
- 0
|
25
31
|
version: "0"
|
26
32
|
type: :runtime
|
27
33
|
version_requirements: *id001
|
@@ -29,10 +35,11 @@ dependencies:
|
|
29
35
|
name: rspec
|
30
36
|
prerelease: false
|
31
37
|
requirement: &id002 !ruby/object:Gem::Requirement
|
32
|
-
none: false
|
33
38
|
requirements:
|
34
39
|
- - ">="
|
35
40
|
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
36
43
|
version: "0"
|
37
44
|
type: :development
|
38
45
|
version_requirements: *id002
|
@@ -40,10 +47,11 @@ dependencies:
|
|
40
47
|
name: watchr
|
41
48
|
prerelease: false
|
42
49
|
requirement: &id003 !ruby/object:Gem::Requirement
|
43
|
-
none: false
|
44
50
|
requirements:
|
45
51
|
- - ">="
|
46
52
|
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 0
|
47
55
|
version: "0"
|
48
56
|
type: :development
|
49
57
|
version_requirements: *id003
|
@@ -57,6 +65,9 @@ extensions: []
|
|
57
65
|
extra_rdoc_files: []
|
58
66
|
|
59
67
|
files:
|
68
|
+
- .gitignore
|
69
|
+
- Gemfile
|
70
|
+
- Gemfile.lock
|
60
71
|
- README.md
|
61
72
|
- bin/zucchini
|
62
73
|
- lib/config.rb
|
@@ -109,6 +120,7 @@ files:
|
|
109
120
|
- templates/project/features/support/masks/retina_ios5.png
|
110
121
|
- templates/project/features/support/screens/welcome.coffee
|
111
122
|
- zucchini.gemspec
|
123
|
+
has_rdoc: true
|
112
124
|
homepage: http://www.zucchiniframework.org
|
113
125
|
licenses: []
|
114
126
|
|
@@ -118,21 +130,23 @@ rdoc_options: []
|
|
118
130
|
require_paths:
|
119
131
|
- lib
|
120
132
|
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
-
none: false
|
122
133
|
requirements:
|
123
134
|
- - ">="
|
124
135
|
- !ruby/object:Gem::Version
|
136
|
+
segments:
|
137
|
+
- 0
|
125
138
|
version: "0"
|
126
139
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
-
none: false
|
128
140
|
requirements:
|
129
141
|
- - ">="
|
130
142
|
- !ruby/object:Gem::Version
|
143
|
+
segments:
|
144
|
+
- 0
|
131
145
|
version: "0"
|
132
146
|
requirements: []
|
133
147
|
|
134
148
|
rubyforge_project:
|
135
|
-
rubygems_version: 1.
|
149
|
+
rubygems_version: 1.3.6
|
136
150
|
signing_key:
|
137
151
|
specification_version: 3
|
138
152
|
summary: Functional testing framework for iOS-powered devices
|