diffux_ci 0.1.0

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: 0e45d93f1149b5ad73fe788533dc5ed95d737732
4
+ data.tar.gz: de408672cf9cb59e357e9f08243b7a03760c60a3
5
+ SHA512:
6
+ metadata.gz: c1f8b36ff77a17db7742e695982346a7f8fc667c715888af935b3696de3bcd6ff589f37c98ca3fc28dc8c1b953929344b84d14d68222fe575374a69bd4bf04a2
7
+ data.tar.gz: 50f65df9ec4732dc4451002c33fdae827c872ebd49a713eaea2a8c09eea67208d4f284a251d8cba0e132fad5355d4a0de137cde60aef9cd5d4e3005a168bd47e
data/bin/diffux_ci ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'diffux_ci_utils'
4
+ require 'diffux_ci_action'
5
+ require 'diffux_ci_uploader'
6
+ require 'fileutils'
7
+
8
+ action = ARGV[0] || 'run'
9
+ case action
10
+ when 'run'
11
+ Thread.abort_on_exception = true
12
+ Thread.new do
13
+ require 'diffux_ci_runner'
14
+ exit
15
+ end
16
+ require 'diffux_ci_server'
17
+
18
+ when 'review'
19
+ system 'open', DiffuxCIUtils.construct_url('/review')
20
+ require 'diffux_ci_server'
21
+
22
+ when 'clean'
23
+ if File.directory? DiffuxCIUtils.config['snapshots_folder']
24
+ FileUtils.remove_entry_secure DiffuxCIUtils.config['snapshots_folder']
25
+ end
26
+
27
+ when 'approve', 'reject'
28
+ abort 'Missing example name' unless example_name = ARGV[1]
29
+ abort 'Missing viewport name' unless viewport_name = ARGV[2]
30
+ DiffuxCIAction.new(example_name, viewport_name).send(action)
31
+
32
+ when 'upload_diffs'
33
+ # `upload_diffs` returns a URL to a static html file
34
+ puts DiffuxCIUploader.new.upload_diffs
35
+ else
36
+ abort "Unknown action \"#{action}\""
37
+ end
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Diffux-CI diffs</title>
5
+ </head>
6
+ <body>
7
+ <h1>Diffux-CI diffs</h1>
8
+ <p>File generated: <%= Time.now %></p>
9
+
10
+ <% diff_images.each do |diff| %>
11
+ <h3>
12
+ <%= diff[:name] %> @ <%= diff[:viewport] %>
13
+ </h3>
14
+ <p><img src="<%= diff[:url] %>"></p>
15
+ <% end %>
16
+ </body>
17
+ </html>
@@ -0,0 +1,29 @@
1
+ require 'diffux_ci_utils'
2
+ require 'fileutils'
3
+
4
+ class DiffuxCIAction
5
+ def initialize(example_name, viewport_name)
6
+ @example_name = example_name
7
+ @viewport_name = viewport_name
8
+ end
9
+
10
+ def approve
11
+ diff_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'diff.png')
12
+ baseline_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'baseline.png')
13
+ candidate_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'candidate.png')
14
+
15
+ FileUtils.rm(diff_path, force: true)
16
+
17
+ if File.exist? candidate_path
18
+ FileUtils.mv(candidate_path, baseline_path)
19
+ end
20
+ end
21
+
22
+ def reject
23
+ diff_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'diff.png')
24
+ candidate_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'candidate.png')
25
+
26
+ FileUtils.rm(diff_path, force: true)
27
+ FileUtils.rm(candidate_path, force: true)
28
+ end
29
+ end
@@ -0,0 +1,116 @@
1
+ require 'selenium-webdriver'
2
+ require 'diffux_core/snapshot_comparer'
3
+ require 'diffux_core/snapshot_comparison_image/base'
4
+ require 'diffux_core/snapshot_comparison_image/gutter'
5
+ require 'diffux_core/snapshot_comparison_image/before'
6
+ require 'diffux_core/snapshot_comparison_image/overlayed'
7
+ require 'diffux_core/snapshot_comparison_image/after'
8
+ require 'chunky_png'
9
+ require 'diffux_ci_utils'
10
+ require 'fileutils'
11
+
12
+ def resolve_viewports(example)
13
+ configured_viewports = DiffuxCIUtils.config['viewports']
14
+
15
+ (example['options']['viewports'] || [configured_viewports.first.first]).map do |viewport|
16
+ configured_viewports[viewport].merge('name' => viewport)
17
+ end
18
+ end
19
+
20
+ begin
21
+ driver = Selenium::WebDriver.for DiffuxCIUtils.config['driver'].to_sym
22
+ rescue Selenium::WebDriver::Error::WebDriverError
23
+ # "unable to obtain stable firefox connection in 60 seconds"
24
+ #
25
+ # This seems to happen sporadically for some versions of Firefox, so we want
26
+ # to retry it in case it will work the second time around.
27
+ driver = Selenium::WebDriver.for DiffuxCIUtils.config['driver'].to_sym
28
+ end
29
+
30
+ begin
31
+ driver.manage.timeouts.script_timeout = 3 # move to config?
32
+ driver.navigate.to DiffuxCIUtils.construct_url('/')
33
+
34
+ # Check for errors during startup
35
+ errors = driver.execute_script('return window.diffux.errors;')
36
+ unless errors.empty?
37
+ fail "JavaScript errors found during initialization: \n#{errors.inspect}"
38
+ end
39
+
40
+ while current = driver.execute_script('return window.diffux.next()') do
41
+ resolve_viewports(current).each do |viewport|
42
+ # Resize window to the right size before rendering
43
+ driver.manage.window.resize_to(viewport['width'], viewport['height'])
44
+
45
+ # Render the example
46
+
47
+ # WebDriver's `execute_async_script` takes a string that is executed in
48
+ # the context of a function. `execute_async_script` injects a callback
49
+ # function as this function's argument here. WebDriver will wait until
50
+ # this callback is called (if it is passed a value it will pass that
51
+ # through to Rubyland), or until WebDriver's `script_timeout` is reached,
52
+ # before continuing. Since we don't define the signature of this function,
53
+ # we can't name the argument so we access it using JavaScript's magic
54
+ # arguments object and pass it down to `renderCurrent()` which calls it
55
+ # when it is done--either synchronously if our example doesn't take an
56
+ # argument, or asynchronously via the Promise and `done` callback if it
57
+ # does.
58
+ script = <<-EOS
59
+ var doneFunc = arguments[arguments.length - 1];
60
+ window.diffux.renderCurrent(doneFunc);
61
+ EOS
62
+ rendered = driver.execute_async_script(script)
63
+
64
+ if rendered['error']
65
+ fail <<-EOS
66
+ Error while rendering "#{current['name']}" @#{viewport['name']}:
67
+ #{rendered['error']}
68
+ Debug by pointing your browser to
69
+ #{DiffuxCIUtils.construct_url('/', name: current['name'])}
70
+ EOS
71
+ end
72
+ output_file = DiffuxCIUtils.path_to(
73
+ current['name'], viewport['name'], 'candidate.png')
74
+
75
+ # Create the folder structure if it doesn't already exist
76
+ unless File.directory?(dirname = File.dirname(output_file))
77
+ FileUtils.mkdir_p(dirname)
78
+ end
79
+
80
+ # Save and crop the screenshot
81
+ driver.save_screenshot(output_file)
82
+ cropped = ChunkyPNG::Image.from_file(output_file)
83
+ cropped.crop!(rendered['left'],
84
+ rendered['top'],
85
+ [rendered['width'], 1].max,
86
+ [rendered['height'], 1].max)
87
+ cropped.save(output_file)
88
+
89
+ print "Checking \"#{current['name']}\" at [#{viewport['name']}]... "
90
+
91
+ # Run the diff if needed
92
+ baseline_file = DiffuxCIUtils.path_to(current['name'], viewport['name'], 'baseline.png')
93
+
94
+ if File.exist? baseline_file
95
+ comparison = Diffux::SnapshotComparer.new(
96
+ ChunkyPNG::Image.from_file(baseline_file),
97
+ cropped
98
+ ).compare!
99
+
100
+ if img = comparison[:diff_image]
101
+ diff_output = DiffuxCIUtils.path_to(current['name'], viewport['name'], 'diff.png')
102
+ img.save(diff_output)
103
+ puts "#{comparison[:diff_in_percent].round(1)}% (#{diff_output})"
104
+ else
105
+ File.delete(output_file)
106
+ puts 'No diff.'
107
+ end
108
+ else
109
+ File.rename(output_file, baseline_file)
110
+ puts "First snapshot created (#{baseline_file})"
111
+ end
112
+ end
113
+ end
114
+ ensure
115
+ driver.quit
116
+ end
@@ -0,0 +1,42 @@
1
+ require 'sinatra/base'
2
+ require 'yaml'
3
+ require 'diffux_ci_utils'
4
+ require 'diffux_ci_action'
5
+
6
+ class DiffuxCIServer < Sinatra::Base
7
+ configure do
8
+ enable :static
9
+ set :port, DiffuxCIUtils.config['port']
10
+ end
11
+
12
+ get '/' do
13
+ @config = DiffuxCIUtils.config
14
+ erb :index
15
+ end
16
+
17
+ get '/review' do
18
+ @snapshots = DiffuxCIUtils.current_snapshots
19
+ erb :review
20
+ end
21
+
22
+ get '/resource' do
23
+ file = params[:file]
24
+ if file.start_with? 'http'
25
+ redirect file
26
+ else
27
+ send_file file
28
+ end
29
+ end
30
+
31
+ post '/reject' do
32
+ DiffuxCIAction.new(params[:name], params[:viewport]).reject
33
+ redirect back
34
+ end
35
+
36
+ post '/approve' do
37
+ DiffuxCIAction.new(params[:name], params[:viewport]).approve
38
+ redirect back
39
+ end
40
+
41
+ run!
42
+ end
@@ -0,0 +1,55 @@
1
+ require 'diffux_ci_utils'
2
+ require 's3'
3
+ require 'securerandom'
4
+
5
+ class DiffuxCIUploader
6
+ BUCKET_NAME = 'diffux_ci-diffs'
7
+
8
+ def initialize
9
+ @s3_access_key_id = DiffuxCIUtils.config['s3_access_key_id']
10
+ @s3_secret_access_key = DiffuxCIUtils.config['s3_secret_access_key']
11
+ end
12
+
13
+ def upload_diffs
14
+ current_snapshots = DiffuxCIUtils.current_snapshots
15
+ return [] if current_snapshots[:diffs].empty?
16
+
17
+ bucket = find_or_build_bucket
18
+
19
+ dir = SecureRandom.uuid
20
+
21
+ diff_images = current_snapshots[:diffs].map do |diff|
22
+ image = bucket.objects.build("#{dir}/#{diff[:name]}_#{diff[:viewport]}.png")
23
+ image.content = open(diff[:file])
24
+ image.content_type = 'image/png'
25
+ image.save
26
+ diff[:url] = image.url
27
+ diff
28
+ end
29
+
30
+ html = bucket.objects.build("#{dir}/index.html")
31
+ html.content =
32
+ ERB.new(
33
+ File.read(File.expand_path(
34
+ File.join(File.dirname(__FILE__), 'diffux_ci-diffs.html.erb')))
35
+ ).result(binding)
36
+ html.content_type = 'text/html'
37
+ html.save
38
+ html.url
39
+ end
40
+
41
+ private
42
+
43
+ def find_or_build_bucket
44
+ service = S3::Service.new(access_key_id: @s3_access_key_id,
45
+ secret_access_key: @s3_secret_access_key)
46
+ bucket = service.buckets.find(BUCKET_NAME)
47
+
48
+ if bucket.nil?
49
+ bucket = service.buckets.build(BUCKET_NAME)
50
+ bucket.save(location: :us)
51
+ end
52
+
53
+ bucket
54
+ end
55
+ end
@@ -0,0 +1,71 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+
4
+ class DiffuxCIUtils
5
+ def self.config
6
+ @@config ||= {
7
+ 'snapshots_folder' => './snapshots',
8
+ 'source_files' => [],
9
+ 'stylesheets' => [],
10
+ 'port' => 4567,
11
+ 'driver' => :firefox,
12
+ 'viewports' => {
13
+ 'large' => {
14
+ 'width' => 1024,
15
+ 'height' => 768
16
+ },
17
+ 'medium' => {
18
+ 'width' => 640,
19
+ 'height' => 888
20
+ },
21
+ 'small' => {
22
+ 'width' => 320,
23
+ 'height' => 444
24
+ }
25
+ }
26
+ }.merge(YAML.load(ERB.new(File.read(
27
+ ENV['DIFFUX_CI_CONFIG_FILE'] || '.diffux_ci.yaml')).result))
28
+ end
29
+
30
+ def self.normalize_name(name)
31
+ name.gsub(/[^a-zA-Z0-9\-_]/, '_')
32
+ end
33
+
34
+ def self.path_to(name, viewport_name, file_name)
35
+ File.join(
36
+ config['snapshots_folder'],
37
+ normalize_name(name),
38
+ "@#{viewport_name}",
39
+ file_name
40
+ )
41
+ end
42
+
43
+ def self.construct_url(absolute_path, params = {})
44
+ params_str = params.map do |key, value|
45
+ "#{key}=#{URI.escape(value)}"
46
+ end.join('&')
47
+ unless params_str.empty?
48
+ params_str = "?#{params_str}"
49
+ end
50
+
51
+ return "http://localhost:#{config['port']}#{absolute_path}#{params_str}"
52
+ end
53
+
54
+ def self.current_snapshots
55
+ prepare_file = lambda do |file|
56
+ viewport_dir = File.expand_path('..', file)
57
+ name_dir = File.expand_path('..', viewport_dir)
58
+ {
59
+ name: File.basename(name_dir),
60
+ viewport: File.basename(viewport_dir).sub('@', ''),
61
+ file: file,
62
+ }
63
+ end
64
+ diff_files = Dir.glob("#{DiffuxCIUtils.config['snapshots_folder']}/**/diff.png")
65
+ baselines = Dir.glob("#{DiffuxCIUtils.config['snapshots_folder']}/**/baseline.png")
66
+ {
67
+ diffs: diff_files.map(&prepare_file),
68
+ baselines: baselines.map(&prepare_file)
69
+ }
70
+ end
71
+ end
@@ -0,0 +1,191 @@
1
+ window.diffux = {
2
+ defined: [],
3
+ currentIndex: 0,
4
+ currentExample: undefined,
5
+ currentRenderedElement: undefined,
6
+ errors: [],
7
+
8
+ define: function(name, func, options) {
9
+ this.defined.push({
10
+ name: name,
11
+ func: func,
12
+ options: options || {}
13
+ });
14
+ },
15
+
16
+ fdefine: function() {
17
+ this.defined = []; // clear out all previously added examples
18
+ this.define.apply(this, arguments); // add the example
19
+ this.define = function() {}; // make `define` a no-op from now on
20
+ },
21
+
22
+ next: function() {
23
+ if (this.currentRenderedElement) {
24
+ if (window.React) {
25
+ window.React.unmountComponentAtNode(document.body.lastChild);
26
+ } else {
27
+ this.currentRenderedElement.parentNode.removeChild(this.currentRenderedElement);
28
+ }
29
+ }
30
+ this.currentExample = this.defined[this.currentIndex];
31
+ if (!this.currentExample) {
32
+ return;
33
+ }
34
+ this.currentIndex++;
35
+ return this.currentExample;
36
+ },
37
+
38
+ setCurrent: function(exampleName) {
39
+ this.defined.forEach(function(example, index) {
40
+ if (example.name === exampleName) {
41
+ this.currentExample = example;
42
+ }
43
+ }.bind(this));
44
+ if (!this.currentExample) {
45
+ throw 'No example found with name "' + exampleName + '"';
46
+ }
47
+ },
48
+
49
+ clearVisibleElements: function() {
50
+ var allElements = Array.prototype.slice.call(document.querySelectorAll('body > *'));
51
+ allElements.forEach(function(element) {
52
+ var style = window.getComputedStyle(element);
53
+ if (style.display !== 'none') {
54
+ element.parentNode.removeChild(element);
55
+ }
56
+ });
57
+ },
58
+
59
+ handleError: function(error) {
60
+ console.error(error);
61
+ return {
62
+ name: this.currentExample.name,
63
+ error: error.message
64
+ };
65
+ },
66
+
67
+ /**
68
+ * @param {Function} func The diffux.describe function from the current
69
+ * example being rendered. This function takes a callback as an argument
70
+ * that is called when it is done.
71
+ * @return {Promise}
72
+ */
73
+ tryAsync: function(func) {
74
+ return new Promise(function(resolve, reject) {
75
+ // Saftey valve: if the function does not finish after 3s, then something
76
+ // went haywire and we need to move on.
77
+ var timeout = setTimeout(function() {
78
+ reject(new Error('Async callback was not invoked within timeout.'));
79
+ }, 3000);
80
+
81
+ // This function is called by the example when it is done executing.
82
+ var doneCallback = function(elem) {
83
+ clearTimeout(timeout);
84
+
85
+ if (!arguments.length) {
86
+ return reject(new Error(
87
+ 'The async done callback expects the rendered element as an ' +
88
+ 'argument, but there were no arguments.'
89
+ ));
90
+ }
91
+
92
+ resolve(elem);
93
+ };
94
+
95
+ func(doneCallback);
96
+ });
97
+ },
98
+
99
+ /**
100
+ * @param {Function} doneFunc injected by driver.execute_async_script in
101
+ * diffux_ci_runner.rb
102
+ */
103
+ renderCurrent: function(doneFunc) {
104
+ try {
105
+ this.clearVisibleElements();
106
+
107
+ var func = this.currentExample.func;
108
+ if (func.length) {
109
+ // The function takes an argument, which is a callback that is called
110
+ // once it is done executing. This can be used to write functions that
111
+ // have asynchronous code in them.
112
+ this.tryAsync(func).then(function(elem) {
113
+ doneFunc(this.processElem(elem));
114
+ }.bind(this)).catch(function(error) {
115
+ doneFunc(this.handleError(error));
116
+ }.bind(this));
117
+ } else {
118
+ // The function does not take an argument, so we can run it
119
+ // synchronously.
120
+ var elem = func();
121
+ doneFunc(this.processElem(elem));
122
+ }
123
+ } catch (error) {
124
+ doneFunc(this.handleError(error));
125
+ }
126
+ },
127
+
128
+ processElem: function(elem) {
129
+ try {
130
+ // TODO: elem.getDOMNode is deprecated in React, so we need to convert
131
+ // this to React.findDOMNode(elem) at some point, or push this requirement
132
+ // into the examples.
133
+ if (elem.getDOMNode) {
134
+ // Soft-dependency to React here. If the thing returned has a
135
+ // `getDOMNode` method, call it to get the real DOM node.
136
+ elem = elem.getDOMNode();
137
+ }
138
+
139
+ this.currentRenderedElement = elem;
140
+
141
+ var width = elem.offsetWidth;
142
+ var height = elem.offsetHeight;
143
+ var top = elem.offsetTop;
144
+ var left = elem.offsetLeft;
145
+
146
+ if (this.currentExample.options.snapshotEntireScreen) {
147
+ width = window.innerWidth;
148
+ height = window.innerHeight;
149
+ top = 0;
150
+ left = 0;
151
+ }
152
+ return {
153
+ name: this.currentExample.name,
154
+ width: width,
155
+ height: height,
156
+ top: top,
157
+ left: left
158
+ };
159
+ } catch (error) {
160
+ return this.handleError(error);
161
+ }
162
+ }
163
+ };
164
+
165
+ window.addEventListener('load', function() {
166
+ var matches = window.location.search.match(/name=([^&]*)/);
167
+ if (!matches) {
168
+ return;
169
+ }
170
+ var example = decodeURIComponent(matches[1]);
171
+ window.diffux.setCurrent(example);
172
+ window.diffux.renderCurrent();
173
+ });
174
+
175
+ // We need to redefine a few global functions that halt execution. Without this,
176
+ // there's a chance that the Ruby code can't communicate with the browser.
177
+ window.alert = function(message) {
178
+ console.log('`window.alert` called', message);
179
+ };
180
+ window.confirm = function(message) {
181
+ console.log('`window.confirm` called', message);
182
+ return true;
183
+ };
184
+ window.prompt = function(message, value) {
185
+ console.log('`window.prompt` called', message, value);
186
+ return null;
187
+ };
188
+
189
+ window.onerror = function(message, url, lineNumber) {
190
+ window.diffux.errors.push({ message: message, url: url, lineNumber: lineNumber });
191
+ }
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style type="text/css">
5
+ * {
6
+ -webkit-transition: none !important;
7
+ -moz-transition: none !important;
8
+ transition: none !important;
9
+ -webkit-animation-duration: 0 !important;
10
+ -moz-animation-duration: 0s !important;
11
+ animation-duration: 0s !important;
12
+ }
13
+ </style>
14
+
15
+ <% @config['stylesheets'].each do |stylesheet| %>
16
+ <link rel="stylesheet" type="text/css"
17
+ href="/resource?file=<%= ERB::Util.url_encode(stylesheet) %>">
18
+ <% end %>
19
+ </head>
20
+ <body style="background-color: #fff; margin: 0; pointer-events: none;">
21
+ <script src="/diffux_ci-runner.js"></script>
22
+ <% @config['source_files'].each do |source_file| %>
23
+ <script src="/resource?file=<%= ERB::Util.url_encode(source_file) %>"></script>
24
+ <% end %>
25
+ </body>
26
+ </html>
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style type="text/css">
5
+ body {
6
+ background-color: #f0f0f0;
7
+ font-family: helvetica, arial;
8
+ }
9
+ form {
10
+ display: inline-block;
11
+ }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <h1>Diffux-CI Review Tool</h1>
16
+ <h2>DIFFS</h2>
17
+ <% @snapshots[:diffs].each do |diff| %>
18
+ <h3>
19
+ <%= diff[:name] %> @ <%= diff[:viewport] %>
20
+ </h3>
21
+ <p><img src="/resource?file=<%= ERB::Util.url_encode(diff[:file]) %>"></p>
22
+ <form style="display: inline-block"
23
+ action="/approve?name=<%= diff[:name] %>&viewport=<%= diff[:viewport] %>"
24
+ method="POST">
25
+ <button type="submit">Approve</button>
26
+ </form>
27
+ <form style="display: inline-block"
28
+ action="/reject?name=<%= diff[:name] %>&viewport=<%= diff[:viewport] %>"
29
+ method="POST">
30
+ <button type="submit">Reject</button>
31
+ </form>
32
+ <% end %>
33
+
34
+ <hr>
35
+
36
+ <h2>BASELINES</h2>
37
+ <% @snapshots[:baselines].each do |baseline| %>
38
+ <h3>
39
+ <%= baseline[:name] %> @ <%= baseline[:viewport] %>
40
+ </h3>
41
+ <p><img src="/resource?file=<%= ERB::Util.url_encode(baseline[:file]) %>"></p>
42
+ <% end %>
43
+ </body>
44
+ </html>
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: diffux_ci
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Henric Trotzig
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: diffux-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.0.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.0.2
33
+ - !ruby/object:Gem::Dependency
34
+ name: chunky_png
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.3'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.3.4
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.3'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.3.4
53
+ - !ruby/object:Gem::Dependency
54
+ name: selenium-webdriver
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '2.44'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.44.0
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '2.44'
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 2.44.0
73
+ - !ruby/object:Gem::Dependency
74
+ name: thin
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '1.6'
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 1.6.3
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.6'
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 1.6.3
93
+ - !ruby/object:Gem::Dependency
94
+ name: sinatra
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '1.4'
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 1.4.5
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.4'
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 1.4.5
113
+ - !ruby/object:Gem::Dependency
114
+ name: s3
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '0.3'
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: 0.3.22
123
+ type: :runtime
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '0.3'
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: 0.3.22
133
+ description: Diffux-CI, a perceptual diff tool for JS components
134
+ email: henric.trotzig@gmail.com
135
+ executables:
136
+ - diffux_ci
137
+ extensions: []
138
+ extra_rdoc_files: []
139
+ files:
140
+ - bin/diffux_ci
141
+ - lib/diffux_ci-diffs.html.erb
142
+ - lib/diffux_ci_action.rb
143
+ - lib/diffux_ci_runner.rb
144
+ - lib/diffux_ci_server.rb
145
+ - lib/diffux_ci_uploader.rb
146
+ - lib/diffux_ci_utils.rb
147
+ - lib/public/diffux_ci-runner.js
148
+ - lib/views/index.erb
149
+ - lib/views/review.erb
150
+ homepage: http://rubygems.org/gems/diffux_ci
151
+ licenses:
152
+ - MIT
153
+ metadata: {}
154
+ post_install_message:
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubyforge_project:
170
+ rubygems_version: 2.4.5
171
+ signing_key:
172
+ specification_version: 4
173
+ summary: Diffux-CI
174
+ test_files: []
175
+ has_rdoc: