motion-screenspecs 0.0.1

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.
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: []