zucchini-ios 0.5.5 → 0.5.6
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/.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
|