motion-screenspecs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0e1ee4821ad5489d2e5e5b0b32e5d189733f0c30
4
+ data.tar.gz: 707ccb1260a5cf1d8607983cfa22bb0639b6451c
5
+ SHA512:
6
+ metadata.gz: d8a7f61d656df6c054faadf2bfd8f8b965bf40b558c6370f0fc169b960893f303600743b9a8f205d920b2d4f6620960ac04b038c3e9518bbebe86991dfa19ab3
7
+ data.tar.gz: 0e17a6418a9631c08ebd252b80d4a4efff5241b8d93d997cb4c2c3739a3ed8f394b3fc33e1eeee3bd5b676a6f33297f6db5a4f549c390b930da18f9af138fa9b
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # motion-screenspecs
2
+
3
+ Test your RubyMotion app regressions using screenshot comparison (similar to [Huxley](https://github.com/facebook/huxley) and [Wraith](https://github.com/BBC-News/wraith)):
4
+
5
+ ```
6
+ $ rake spec
7
+
8
+ AppScreenshots
9
+ - should take screenshots
10
+
11
+ AppScreenshots.menu
12
+ - should be <= 5.0% difference
13
+
14
+ AppScreenshots.timeline
15
+ - should be <= 5.0% difference [FAILED - was 10.75%]
16
+
17
+ Bacon::Error: was 10.75%
18
+ spec.rb:698:in `satisfy:': AppScreenshots.timeline - should be <= 5.0% difference
19
+ spec.rb:438:in `execute_block'
20
+ spec.rb:402:in `run_postponed_block:'
21
+ spec.rb:397:in `resume'
22
+
23
+ 3 specifications (3 requirements), 1 failures, 0 errors
24
+ ```
25
+
26
+ ![exampke](readme.png)
27
+
28
+ (calculated diffs are truer than visual representation)
29
+
30
+ ## Installation
31
+
32
+ Add this line to your application's Gemfile:
33
+
34
+ gem 'motion-screenspecs'
35
+
36
+ And then execute:
37
+
38
+ $ bundle
39
+
40
+ Or install it yourself as:
41
+
42
+ $ gem install motion-screenspecs
43
+
44
+ ## Usage
45
+
46
+ motion-screenspecs works in unison with [motion-screenshots](https://github.com/usepropeller/motion-screenshots). Here are the steps you need to get everything working:
47
+
48
+ 1. Create a subclass of `Motion::Screenshots::Base` (see the [motion-screenshots README](https://github.com/usepropeller/motion-screenshots/blob/master/README.md)).
49
+
50
+ 2. Create the following directory structure:
51
+
52
+ ```
53
+ spec/
54
+ screenshots/
55
+ [YourScreenshotSubclass]
56
+ expectations/
57
+ [title of your screenshot].png
58
+ [title of your other screenshot].png
59
+ [etc].png
60
+ ```
61
+
62
+ The images in `expectations` are known values for your app. You can take these screenshots manually or using motion-screenshots' `rake screenshots`.
63
+
64
+ Failing image diffs will be saved in `spec/screenshots/[YourScreenshotSubclass]/failures`. All results from the latest test are saved to `spec/screenshots/[YourScreenshotSubclass]/results`.
65
+
66
+ 3. Add a call to `tests_screenshots` in your specs:
67
+
68
+ ```ruby
69
+ describe "Screenshots" do
70
+ tests_screenshots AppScreenshots
71
+ end
72
+ ```
73
+
74
+ The [sample app](sample) is a complete example with a failing test.
75
+
76
+ Hats off to [Jeff Kreeftmeijer](http://jeffkreeftmeijer.com/2011/comparing-images-and-creating-image-diffs/) for the image diffing help!
77
+
78
+ ## Configuration
79
+
80
+ There's a couple of configuration options you can use:
81
+
82
+ ```ruby
83
+ Motion::Project::App.setup do |app|
84
+ # Set your tolerance % for image differences
85
+ Motion::Screenspecs.set_tolerance(5.0, app)
86
+
87
+ # Set how long the tests will wait for your screenshots to finish
88
+ Motion::Screenspecs.set_screenshot_timeout(120, app)
89
+
90
+ # Set how long the tests will wait for a given image to finish diffing
91
+ Motion::Screenspecs.set_diff_timeout(20, app)
92
+
93
+ # Set whether or not your failed diffs will open in Finder upon finishing tests
94
+ Motion::Screenspecs.open_failures_at_exit = true
95
+ end
96
+ ```
97
+
98
+ `rake spec`
99
+
100
+ ## Contributing
101
+
102
+ 1. Fork it
103
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
104
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
105
+ 4. Push to the branch (`git push origin my-new-feature`)
106
+ 5. Create new Pull Request
107
+
108
+ ## Contact
109
+
110
+ [Clay Allsopp](http://clayallsopp.com/)
111
+ [@clayallsopp](https://twitter.com/clayallsopp)
@@ -0,0 +1,207 @@
1
+ unless defined?(Motion::Project::Config)
2
+ raise "This file must be required within a RubyMotion project Rakefile."
3
+ end
4
+
5
+ require 'motion-screenshots'
6
+ require 'motion-env'
7
+
8
+ require 'webrick'
9
+ require 'oily_png'
10
+ require 'fileutils'
11
+ require 'shellwords'
12
+
13
+ is_spec_mode = Rake.application.top_level_tasks.include?('spec')
14
+
15
+ if is_spec_mode
16
+ class Motion::Project::Config
17
+
18
+ def spec_core_files_with_screenshots
19
+ files = spec_core_files_without_screenshots
20
+
21
+ lib_helper = File.join(File.dirname(__FILE__), 'spec', 'helpers.rb')
22
+ files << lib_helper unless files.include?(lib_helper)
23
+ files
24
+ end
25
+
26
+ alias_method "spec_core_files_without_screenshots", "spec_core_files"
27
+ alias_method "spec_core_files", "spec_core_files_with_screenshots"
28
+ end
29
+
30
+ class Motion::Project::App
31
+ class << self
32
+ def build_with_screenspecs(platform, opts = {})
33
+ unless File.exist?('vendor/Pods/KSScreenshotManager')
34
+ Rake::Task["pod:install"].reenable
35
+ Rake::Task["pod:install"].invoke
36
+ end
37
+ build_without_screenspecs(platform, opts)
38
+ end
39
+
40
+ alias_method "build_without_screenspecs", "build"
41
+ alias_method "build", "build_with_screenspecs"
42
+ end
43
+ end
44
+ end
45
+
46
+ module Motion
47
+ module Screenspecs
48
+ PORT = 9678
49
+ TOLERANCE_ENV = '_motion-screenspecs-tolerance'
50
+ SCREENSHOT_TIMEOUT_ENV = '_motion-screenspecs-screenshot-timeout'
51
+ DIFF_TIMEOUT_ENV = '_motion-screenspecs-diff-timeout'
52
+
53
+ def self.tolerance
54
+ @tolerance
55
+ end
56
+
57
+ def self.set_tolerance(tolerance, config)
58
+ @tolerance = tolerance
59
+ config.env[TOLERANCE_ENV] = tolerance
60
+ tolerance
61
+ end
62
+
63
+ def self.set_screenshot_timeout(timeout, config)
64
+ config.env[SCREENSHOT_TIMEOUT_ENV] = timeout
65
+ end
66
+
67
+ def self.set_diff_timeout(timeout, config)
68
+ config.env[DIFF_TIMEOUT_ENV] = timeout
69
+ end
70
+
71
+ def self.open_failures_at_exit?
72
+ !!@open_failures_at_exit
73
+ end
74
+
75
+ def self.open_failures_at_exit=(open)
76
+ @open_failures_at_exit = open
77
+ end
78
+
79
+ def self.failures
80
+ @failures ||= []
81
+ end
82
+
83
+ def self.screenshots_root(screenshot_class)
84
+ "spec/screenshots/#{screenshot_class}"
85
+ end
86
+
87
+ def self.start_server!
88
+ # Start a web server to bounce file paths to-and-from the
89
+ # RubyMotion-CRuby barrier
90
+ @web_server ||= begin
91
+ server = WEBrick::HTTPServer.new(:Port => Motion::Screenspecs::PORT, :Logger => WEBrick::Log.new("/dev/null"), :AccessLog => [])
92
+ server.mount '/', Motion::Screenspecs::Servlet
93
+ at_exit {
94
+ server.shutdown
95
+ }
96
+ Thread.start do
97
+ server.start
98
+ end
99
+ server
100
+ end
101
+ end
102
+
103
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet
104
+ def do_GET (request, response)
105
+ if (title = request.query["title"]) &&
106
+ (screenshot_path = request.query["screenshot_path"]) &&
107
+ (screenshot_class = request.query['screenshot_class'])
108
+ screenshot_path.gsub!("file://", "")
109
+ screenshots_root = Motion::Screenspecs.screenshots_root(screenshot_class)
110
+ expectation_path = File.expand_path(File.join(screenshots_root, "expectations", "#{title}.png"))
111
+ result_path = File.expand_path(File.join(screenshots_root, "results", "#{title}.png"))
112
+ failure_path = File.expand_path(File.join(screenshots_root, "failures", "#{title}.png"))
113
+ FileUtils.mkdir_p(File.dirname(failure_path))
114
+ FileUtils.mkdir_p(File.dirname(result_path))
115
+
116
+ File.delete(result_path) if File.exists?(result_path)
117
+ temp_result_path = File.join(File.dirname(result_path), File.basename(screenshot_path))
118
+ FileUtils.cp(screenshot_path, File.dirname(result_path))
119
+ FileUtils.mv(temp_result_path, result_path)
120
+
121
+ File.delete(failure_path) if File.exists?(failure_path)
122
+
123
+ percentage = Motion::Screenspecs::ImageDiff.new.percentage(expectation_path, screenshot_path, failure_path)
124
+ success = percentage < Motion::Screenspecs.tolerance
125
+ response.status = success ? 200 : 400
126
+ response.content_type = "text/plain"
127
+ response.body = "%05.2f" % percentage
128
+ else
129
+ response.status = 404
130
+ response.body = "You did not provide the correct parameters"
131
+ end
132
+ end
133
+ end
134
+
135
+ class ImageDiff
136
+ include ChunkyPNG::Color
137
+
138
+ # via http://jeffkreeftmeijer.com/2011/comparing-images-and-creating-image-diffs/
139
+ def percentage(image_a, image_b, failure_path)
140
+ images = [
141
+ ChunkyPNG::Image.from_file(image_a),
142
+ ChunkyPNG::Image.from_file(image_b)
143
+ ]
144
+
145
+ output = ChunkyPNG::Image.new(images.first.width, images.last.height, WHITE)
146
+
147
+ diff = []
148
+
149
+ images.first.height.times do |y|
150
+ images.first.row(y).each_with_index do |pixel, x|
151
+ unless pixel == images.last[x,y]
152
+ score = Math.sqrt(
153
+ (r(images.last[x,y]) - r(pixel)) ** 2 +
154
+ (g(images.last[x,y]) - g(pixel)) ** 2 +
155
+ (b(images.last[x,y]) - b(pixel)) ** 2
156
+ ) / Math.sqrt(MAX ** 2 * 3)
157
+
158
+ output[x,y] = rgb(
159
+ r(pixel) + r(images.last[x,y]) - 2 * [r(pixel), r(images.last[x,y])].min,
160
+ g(pixel) + g(images.last[x,y]) - 2 * [g(pixel), g(images.last[x,y])].min,
161
+ b(pixel) + b(images.last[x,y]) - 2 * [b(pixel), b(images.last[x,y])].min
162
+ )
163
+ diff << score
164
+ end
165
+ end
166
+ end
167
+
168
+ summed = diff.inject {|sum, value| sum + value}
169
+ if summed
170
+ percent = (summed / images.first.pixels.length) * 100
171
+ else
172
+ percent = 0
173
+ end
174
+ if percent >= Motion::Screenspecs.tolerance
175
+ output.save(failure_path)
176
+ Motion::Screenspecs.failures << failure_path
177
+ end
178
+ percent
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ lib_dir_path = File.dirname(File.expand_path(__FILE__))
185
+ Motion::Project::App.setup do |app|
186
+ app.files.unshift(Dir.glob(File.join(lib_dir_path, "motion/**/*.rb")))
187
+
188
+ Motion::Screenspecs.set_tolerance(5.0, app)
189
+ Motion::Screenspecs.set_screenshot_timeout(120, app)
190
+ Motion::Screenspecs.set_diff_timeout(20, app)
191
+ Motion::Screenspecs.open_failures_at_exit = true
192
+
193
+ if is_spec_mode
194
+ app.pods do
195
+ pod 'KSScreenshotManager'
196
+ end
197
+
198
+ Motion::Screenspecs.start_server!
199
+
200
+ at_exit {
201
+ if Motion::Screenspecs.open_failures_at_exit?
202
+ folder_path = Dir.glob('spec/**/failures').first
203
+ `open #{folder_path.shellescape}` unless Motion::Screenspecs.failures.empty?
204
+ end
205
+ }
206
+ end
207
+ end
@@ -0,0 +1,76 @@
1
+ module Bacon
2
+ class Context
3
+ def tests_screenshots(screenshot_class)
4
+ describe screenshot_class do
5
+ shared_screenshots = screenshot_class.send(:shared)
6
+ shared_screenshots.exitOnComplete = false
7
+ if shared_screenshots.respond_to?("isLoggingEnabled")
8
+ shared_screenshots.loggingEnabled = false
9
+ end
10
+ tolerance = ENV['_motion-screenspecs-tolerance']
11
+ screenshot_timeout = ENV['_motion-screenspecs-screenshot-timeout']
12
+ diff_timeout = ENV['_motion-screenspecs-diff-timeout']
13
+
14
+ it "should take screenshots" do
15
+ existing_after = shared_screenshots.screenshot_groups.last.instance_variable_get("@after_actions")
16
+ shared_screenshots.screenshot_groups.last.after do
17
+ existing_after.call if existing_after
18
+
19
+ # resume tests
20
+ resume
21
+ end
22
+
23
+ screenshot_class.start!
24
+ wait_max screenshot_timeout do
25
+
26
+ true.should == true
27
+ end
28
+ end
29
+
30
+ shared_screenshots.screenshot_groups.each do |group|
31
+ title = group.instance_variable_get('@title')
32
+ before do
33
+ path_url = shared_screenshots.screenshotsURL
34
+
35
+ is_retina = UIScreen.mainScreen.scale != 1.0
36
+ density = is_retina ? "@2x" : ""
37
+ device_prefix = nil
38
+
39
+ if UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad
40
+ device_prefix = "ipad"
41
+ elsif UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone
42
+ device_prefix = "iphone#{CGRectGetHeight(UIScreen.mainScreen.bounds).to_i}"
43
+ end
44
+
45
+ device_prefix << density
46
+
47
+ file_name = "#{device_prefix}-#{NSLocale.currentLocale.localeIdentifier}-#{title}.png"
48
+ path = path_url.URLByAppendingPathComponent(file_name)
49
+ @screenshot_path = path.absoluteString
50
+ end
51
+
52
+ describe "#{screenshot_class}.#{title}" do
53
+ it "should be < #{tolerance}% difference" do
54
+
55
+ url = NSURL.URLWithString "http://localhost:9678?screenshot_class=#{screenshot_class}&title=#{title}&screenshot_path=#{@screenshot_path}"
56
+ request = NSURLRequest.requestWithURL(url)
57
+ NSURLConnection.sendAsynchronousRequest(request, queue:NSOperationQueue.mainQueue,
58
+ completionHandler:->(ns_url_response, data, error){
59
+ @response = ns_url_response
60
+ @data = data
61
+ resume
62
+ })
63
+
64
+ wait_max diff_timeout do
65
+ body = NSString.alloc.initWithData(@data, encoding:NSUTF8StringEncoding)
66
+ @response.statusCode.should.satisfy("was #{body}%") {|object|
67
+ object == 200
68
+ }
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: motion-screenspecs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Clay Allsopp
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: motion-screenshots
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.5
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: oily_png
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: motion-env
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.0.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.0.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Test your RubyMotion apps using screenshot comparison.
70
+ email:
71
+ - clay.allsopp@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - README.md
77
+ - lib/motion-screenspecs.rb
78
+ - lib/spec/helpers.rb
79
+ homepage: https://github.com/usepropeller/motion-screenspecs
80
+ licenses:
81
+ - MIT
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 2.0.3
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Test your RubyMotion apps using screenshot comparison.
103
+ test_files: []